Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,58 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- PHP 8.2+ compatibility
- Use Payment Request API from Sylius
- New Unified Authentication System (OAuth2)
- **Multi-shop support**: several gateway configurations of the same type may now coexist, each
connected to its own PayPlug account and scoped to its own channels
- The connected PayPlug account is displayed on each gateway's update screen
- "Disconnect this account" per gateway, clearing that gateway's credentials without touching others
- Channels already claimed by another enabled gateway of the same type are rendered unselectable

> [!IMPORTANT]
> Merchants will need to contact support to switch to the new authentication method.

### Changed
- Plugin structure has been changed to follow the new Symfony bundle structure
- Front assets have been migrated to use Stimulus
- Gateway uniqueness is now enforced **per channel** instead of per installation: two gateways of
the same type may both be enabled as long as their channel sets are disjoint
- Credentials are resolved from the payment method rather than from the gateway factory name, so a
request for one channel can no longer be signed with another channel's account

### Removed
- Drop Payum support
- Drop Sylius 1.x support
- Drop usage of Secret key - Use OAuth2 instead

### Fixed
- Integrated Payment no longer accepts a payment method id that does not belong to the order's
channel, or a disabled one — previously a shopper could have the payment created on another
merchant's PayPlug account
- Oney instalment options, the Oney availability check and Apple Pay now resolve the gateway serving
the current channel instead of an arbitrary one

### Breaking changes for anyone extending the plugin

| Removed / changed | Replacement |
| --- | --- |
| `PayPlugApiClientFactoryInterface::create(string $factoryName)` | `createForPaymentMethod(PaymentMethodInterface $pm)` |
| `UnifiedApiPaymentCreatorInterface::createPayment($dto)` | `createPayment($dto, PaymentMethodInterface $method)` |
| `OperationStatusFetcherInterface::getOperation($id)` | `getOperation($id, PaymentMethodInterface $method)` |
| `AbstractGatewayConfigurationType::__construct()` — `$gatewayConfigRepository` and `$requestStack` dropped | translator only |
| `shouldValidateBaseCurrency()` / `baseCurrencyViolationMessage()` — `protected` → `public`, now take the **mapped** config | same hooks, new visibility/shape |
| `$gatewayFactoryName` property on the 8 configuration types | no longer read; the factory name comes off the gateway config |
| Translation key `form.only_one_gateway_allowed` | `form.gateway_channel_conflict` (`%channel%`, `%payment_method%`) |
| Injecting `PayplugUnifiedCore\Contracts\IConfigurationRepository` (its service alias is gone) | `ScopedConfigurationRepositoryInterface`, scoped per payment method |
| `PaymentMethodRepositoryInterface::findOneByGatewayName()` — **deprecated**, returns an arbitrary config when several share a factory name | `findOneEnabledByGatewayNameAndChannel($factoryName, $channel)` |
| `OneyExtension::__construct()` — `$gatewayConfigRepository` dropped, `$paymentMethodRepository` is now the plugin's `PaymentMethodRepositoryInterface` | inject the plugin repository |
| `OneySupportedPaymentChoiceProvider::__construct()` | now also takes a `ChannelContextInterface` |

Requires `payplug/unified-plugin-core ^1.1.2` (for the nullable `TokenOutput::$idToken`).

> [!NOTE]
> A gateway connected before this release shows a "re-authenticate" placeholder instead of the
> account email until the merchant reconnects — the address is only available from the interactive
> OAuth `id_token`, which is minted at login.

Please refer to [github releases](https://github.com/payplug/SyliusPayPlugPlugin/releases) for historical release information.

---
Expand Down
32 changes: 32 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
# Deprecations

## The Payum wiring — removal planned for early 2027

The plugin no longer uses Payum. The live payment flow is Sylius 2.x command/response providers,
declared in `config/services.yaml` and implemented in `src/Command/Handler/`.

The Payum code left in the tree is inert: `PayPlugSyliusPayPlugExtension::load()` does not load
`config/services/gateway.xml`, which holds the only `payum.gateway_factory_builder` tags, so no
Payum gateway is ever registered for the `payplug*` factory names. The actions in `src/Action/` are
still tagged `payum.action` in the compiled container (via `#[AutoconfigureTag]` attributes on the
classes), but with no gateway to attach to they are never executed.

The following are deprecated and **will be removed permanently in early 2027**:

- `PayPlug\SyliusPayPlugPlugin\Action\CaptureAction`
- `PayPlug\SyliusPayPlugPlugin\Action\StatusAction`
- `PayPlug\SyliusPayPlugPlugin\Action\NotifyAction`
- `PayPlug\SyliusPayPlugPlugin\Action\ConvertPaymentAction`
- `PayPlug\SyliusPayPlugPlugin\Action\Api\ApiAwareTrait::setApi()` (the trait's `$payPlugApiClient`
property is **not** deprecated — it is still used by `Controller\OneClickAction`)
- `PayPlug\SyliusPayPlugPlugin\Gateway\AbstractGatewayFactory::populateConfig()` (the class itself
is **not** deprecated — it remains the home of `FACTORY_NAME`, `FACTORY_TITLE` and
`BASE_CURRENCY_CODE`)
- `config/services/gateway.xml`

**If you do not import `config/services/gateway.xml` in your own application, this affects you in no
way** — none of the above runs today, and there is nothing to migrate.

If you *do* import it to keep using Payum, that escape hatch stops working at the removal date. Move
to the command handlers in `src/Command/Handler/` before then.

# Upgrading from 1.0.0

1. Skip the faulty migration
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"ext-json": "*",
"giggsey/libphonenumber-for-php": "^8.12",
"payplug/payplug-php": "^4.0",
"payplug/unified-plugin-core": "^1.1.0",
"payplug/unified-plugin-core": "^1.1.2",
"php-http/message-factory": "^1.1",
"sylius/refund-plugin": "^2.0",
"sylius/sylius": "^2.0",
Expand Down
14 changes: 7 additions & 7 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 14 additions & 1 deletion config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ services:
$unifiedApiBaseUrl: '%payplug.unified_api_base_url%'
$unifiedApiVerifyTls: '%payplug.unified_api_verify_tls%'

# `security.csrf.token_manager` is only registered while CSRF protection is enabled, so it is
# injected with `@?`: a hard reference would break container compilation for an application
# that turns it off, rather than degrading the way Sylius's own admin actions do.
PayPlug\SyliusPayPlugPlugin\Action\Admin\Auth\UnifiedLogoutController:
arguments:
$csrfTokenManager: '@?security.csrf.token_manager'

PayPlug\SyliusPayPlugPlugin\Repository\PaymentRepositoryInterface:
class: PayPlug\SyliusPayPlugPlugin\Repository\PaymentRepository
parent: sylius.repository.payment
Expand Down Expand Up @@ -59,7 +66,13 @@ services:
PayplugUnifiedCore\Contracts\IUnifiedApiHttpClient:
alias: PayPlug\SyliusPayPlugPlugin\Upc\SyliusUnifiedApiHttpClient

PayplugUnifiedCore\Contracts\IConfigurationRepository:
# Deliberately NOT aliased: PayplugUnifiedCore\Contracts\IConfigurationRepository carries no
# account context on any of its methods, and since PRE-3628 a merchant may hold several CB
# gateway configs — an unscoped SyliusUpcConfigurationRepository throws on every read. Aliasing
# it would let a future UPC consumer autowire a service that blows up at runtime, in whatever
# code path UPC happened to choose; without the alias it fails at container build instead.
# Consumers take ScopedConfigurationRepositoryInterface and scope it to their payment method.
PayPlug\SyliusPayPlugPlugin\Upc\ScopedConfigurationRepositoryInterface:
alias: PayPlug\SyliusPayPlugPlugin\Upc\SyliusUpcConfigurationRepository

PayplugUnifiedCore\Contracts\IPaymentRepository:
Expand Down
21 changes: 19 additions & 2 deletions config/services/gateway.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
<?xml version="1.0" ?>
<!--
This service file was used to register payum gateways.
We don't use payum anymore, but if mandatory, you can import this file to register them and still use payum
DEPRECATED / INERT — this file is not loaded, and nothing in it runs by default.

It was used to register the Payum gateways. The plugin no longer uses Payum: the live payment
flow is Sylius 2.x command/response providers, wired in config/services.yaml and implemented in
src/Command/Handler/. PayPlugSyliusPayPlugExtension::load() loads exactly two files,
config/services.yaml and config/services/client.xml — never this one.

This file holds the only `payum.gateway_factory_builder` tags in the repository, so with it
unloaded no Payum gateway exists for the `payplug*` factory names. Note that the four actions in
src/Action/ ARE tagged `payum.action` in the compiled container, via #[AutoconfigureTag]
attributes on the classes themselves — but with no gateway to attach to, those tags are
collected and never used, and the actions never execute. Do not reason about the payment flow
from src/Action/; see src/Command/Handler/ instead.

It is kept as an opt-in escape hatch: a host application that still needs Payum can import this
file itself to register the gateways. Removing it would therefore be a BC break, so it is
deprecated rather than deleted.

Will be removed permanently, along with src/Action/, in early 2027. See PRE-3684.
-->
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
Expand Down
9 changes: 9 additions & 0 deletions config/twig_hooks/admin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,30 @@ sylius_twig_hooks:
renew_oauth: &renewOAuth
template: '@PayPlugSyliusPayPlugPlugin/admin/payment_method/form/renew_oauth.html.twig'
priority: -5
connected_account: &connectedAccount
template: '@PayPlugSyliusPayPlugPlugin/admin/payment_method/form/connected_account.html.twig'
priority: -4
'sylius_admin.payment_method.update.content.form.sections.gateway_configuration.payplug_oney':
<<: *oneyGateway
renew_oauth: *renewOAuth
connected_account: *connectedAccount
'sylius_admin.payment_method.update.content.form.sections.gateway_configuration.payplug_bancontact':
<<: *bancontactGateway
renew_oauth: *renewOAuth
connected_account: *connectedAccount
'sylius_admin.payment_method.update.content.form.sections.gateway_configuration.payplug_apple_pay':
<<: *applePayGateway
renew_oauth: *renewOAuth
connected_account: *connectedAccount
'sylius_admin.payment_method.update.content.form.sections.gateway_configuration.payplug_american_express':
<<: *amexGateway
renew_oauth: *renewOAuth
connected_account: *connectedAccount
'sylius_admin.payment_method.update.content.form.sections.gateway_configuration.payplug_scalapay':
<<: *scalapayGateway
renew_oauth: *renewOAuth
connected_account: *connectedAccount
'sylius_admin.payment_method.update.content.form.sections.gateway_configuration.payplug_wero':
<<: *weroGateway
renew_oauth: *renewOAuth
connected_account: *connectedAccount
10 changes: 2 additions & 8 deletions ruleset/phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -665,14 +665,8 @@ parameters:
path: ../src/Gateway/AbstractGatewayFactory.php

-
message: '#^Cannot call method add\(\) on mixed\.$#'
identifier: method.nonObject
count: 1
path: ../src/Gateway/Form/Type/AbstractGatewayConfigurationType.php

-
message: '#^Cannot call method getId\(\) on mixed\.$#'
identifier: method.nonObject
message: '#^PHPDoc tag @SuppressWarnings has invalid value \(\(PHPMD\.UnusedFormalParameter\)\)\: Unexpected token "\.UnusedFormalParameter\)", expected ''\)'' at offset 554 on line 11$#'
identifier: phpDoc.parseError
count: 1
path: ../src/Gateway/Form/Type/AbstractGatewayConfigurationType.php

Expand Down
9 changes: 9 additions & 0 deletions src/Action/Admin/Auth/UnifiedAuthenticationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Doctrine\ORM\EntityManagerInterface;
use Payplug\Authentication;
use Payplug\Payplug;
use PayPlug\SyliusPayPlugPlugin\Auth\IdTokenEmailExtractor;
use PayPlug\SyliusPayPlugPlugin\Validator\PaymentMethodValidator;
use PayplugUnifiedCore\Auth\OAuth2Client;
use PayplugUnifiedCore\Contracts\IOAuthHttpClient;
Expand Down Expand Up @@ -44,6 +45,7 @@ public function __construct(
private PaymentMethodValidator $paymentMethodValidator,
private LoggerInterface $logger,
private IOAuthHttpClient $oauthHttpClient,
private IdTokenEmailExtractor $idTokenEmailExtractor,
private string $payplugOauthBaseUrl,
private string $payplugOauthAudience,
) {
Expand Down Expand Up @@ -125,6 +127,13 @@ public function oauthCallback(Request $request): Response
$config = $gatewayConfig->getConfig();
$config['live_client'] = $liveClientDataResult['httpResponse'] ?? null;
$config['test_client'] = $testClientDataResult['httpResponse'] ?? null;
// Who just authorized, for the admin screen to show. The id_token is the only carrier
// of that identity — /account has no email field and the client-credentials token used
// for every later API call names no user — and it is discarded with $token when this
// method returns, so it is captured here or not at all. Overwritten unconditionally,
// including with null: after a re-auth against a different PayPlug account, keeping the
// previous address would misreport which account is taking the money.
$config['account_email'] = $this->idTokenEmailExtractor->extract($token->idToken);
$gatewayConfig->setConfig($config);

$this->entityManager->flush();
Expand Down
Loading
Loading