Skip to content
Open
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
132 changes: 132 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Copilot Instructions

These instructions apply to code reviews on pull requests in this repository.

## Project Context

This is PayPlug's official Sylius plugin. PayPlug is a Payment Service Provider (PSP). The plugin integrates PayPlug's payment processing into Sylius via multiple payment methods: standard card (PayPlug), Oney financing, Bancontact, Scalapay, Apple Pay, and American Express.

**Stack**: PHP 8.2+, Sylius 2.0+, Symfony 6.4+, Payum, `payplug/payplug-php ^4.0`

## Intentional Patterns β€” Do Not Flag as Issues

- **`sleep(10)` in `NotifyAction`** β€” intentional, prevents a race condition between the IPN webhook and the user redirect. Never suggest removing it.
- **No direct SDK calls** β€” `PayPlugApiClient` is the only allowed entry point to the PayPlug PHP SDK. Any direct call to SDK classes bypasses this and is a bug.
- **Dual architecture** β€” Sylius 2.1+ uses a command/response provider model alongside legacy Payum actions. Both coexist intentionally.
- **Card saving condition** β€” a card is only saved when the PayPlug API response includes `metadata['customer_id']`. Absence of this guard is a bug.
- **`payment_context.cart`** β€” required in the PayPlug API payload for Oney and Scalapay payments. Missing it causes API rejection.
- **Distributed lock in `NotifyAction`** β€” Symfony Lock is used on payment ID to prevent concurrent webhook processing. Do not flag as over-engineering.

## Code Review Dimensions

### Security
- SQL injection, XSS, CSRF
- Authentication and authorization flaws
- Secrets or credentials committed in code
- Insecure deserialization, path traversal, SSRF
- Direct calls to PayPlug PHP SDK classes instead of going through `PayPlugApiClient`
- Webhook payloads must be verified via `PayPlugApiClient::treat()` before any processing β€” never act on raw `php://input` directly
- Card data (PAN, CVV, raw card numbers) must never appear in logs, error messages, or stored fields
- API secret keys must never appear in logs, exception messages, or HTTP responses
- Payment amounts must be validated server-side β€” never trust a client-submitted amount
- `redirect_url` values must come from the PayPlug API response, never constructed from user input (open redirect risk)

### Performance
- N+1 queries (especially in Sylius entity traversal)
- Unnecessary memory allocations
- Algorithmic complexity (O(nΒ²) in hot paths)
- Missing database indexes
- Unbounded queries or loops
- Resource leaks

### Correctness
- Edge cases: empty input, null, overflow
- Race conditions and concurrency issues
- Error handling and propagation
- Off-by-one errors, type safety
- `declare(strict_types=1)` must be present in every PHP file
- For new payment gateways (PPRO pattern): verify the full implementation checklist is covered (gateway factory, form type, resolver decorator, refund provider, templates, service definitions, translations)
- Amount unit: Payum works in the smallest currency unit (cents). Any conversion must be explicit β€” silently mixing units is a payment amount bug
- EUR-only enforcement must happen before the API call, not after
- `factory_name` must be stored alongside `redirect_url` in payment details β€” missing it allows a stale redirect from one gateway to be reused when retrying with a different one
- Refund amounts must not exceed the remaining refundable amount on the payment
- State machine transition names must match Sylius/Payum constants exactly; Oney adds a custom `oney_request_payment` transition that is easy to misspell or omit

### Maintainability
- Naming clarity, single responsibility, duplication
- Test coverage: PHPUnit in `tests/PHPUnit/`, Behat in `features/`
- Documentation for non-obvious logic only β€” do not flag missing comments on self-explanatory code
- PHPStan level max compliance; suppressions must go in `ruleset/phpstan-baseline.neon`
- ECS coding standard based on `sylius-labs/coding-standard`
- Translations must be complete in all three locales (`en`, `fr`, `it`) β€” partial translation is a regression
- New services must be registered in both `config/services.yaml` (command/response providers) and the relevant `config/services/*.xml` file (Payum factory or API client) β€” registering only one side causes runtime failures

### Headless Compliance

In a headless Sylius setup the frontend is a decoupled SPA or mobile app β€” it cannot follow server-side HTTP redirects. Any shop-facing response that performs a redirect instead of returning JSON breaks the headless flow.

- Shop-facing controllers and response providers must return a `JsonResponse` containing a `redirect_url` field rather than a `RedirectResponse`. The client is responsible for performing the redirect.
- Known existing offenders (do not flag these as new issues, but flag any new code that replicates the same pattern):
- `src/OrderPay/Provider/CaptureHttpResponseProvider.php` β€” returns `RedirectResponse($data['redirect_url'])`
- `src/Controller/OneClickAction.php` β€” returns `RedirectResponse` on all exit paths
- Admin-facing controllers (`src/Action/Admin/`) are exempt β€” headless compliance only applies to the shop payment flow.
- If a new controller or response provider in the shop flow returns `RedirectResponse`, flag it as a headless compliance issue.

## Output Format

Structure the review comment exactly as follows:

### 1. What's Good

A bullet list of positive observations β€” things done well, non-obvious correct decisions, solid patterns.

---

### 2. Summary table

A markdown table with two columns: **Dimension** and **Rating**. One row per review dimension. Use emoji inline with the rating text:

| Dimension | Rating |
|---|---|
| Security | βœ… Fine |
| Correctness | ⚠️ Medium (short reason) |
| Performance | βœ… Fine |
| Maintainability | ⚠️ Low (short reason) |

Severity scale:
- βœ… **Fine** β€” no issues
- ⚠️ **Low / Medium** β€” should be fixed but not blocking
- ❌ **High / Critical** β€” must be fixed before merge

---

### 3. Closing one-liner

A single sentence summarising what needs to be addressed before merge (or that the PR is ready if nothing critical).

---

### 4. Individual findings (one section per issue)

Each finding follows this exact structure:

**Heading:** `[Dimension] [emoji] [Severity]` β€” e.g. `Security ⚠️ Medium`

**Subtitle (bold):** short title followed by the file path and line number as a markdown link β€” e.g. `**Path traversal in getPayment** (PaymentClient.php:290)`

**Code block:** the relevant snippet from the diff showing the problem.

**Explanation paragraph:** what the risk is and why it matters. Be concrete.

**Fix line:** start with `Fix:` in bold, then a brief description, followed by a code block showing the suggested fix.

Lead with Critical/High findings. Omit the findings section entirely if there are no issues.

## Iterative Reviews

When reviewing a new commit on a PR that already has open review threads:

- **Resolve threads** for issues that have been addressed in the new commit β€” do not leave them open if the fix is present.
- **Do not re-open or re-comment** on issues that were already resolved in a previous round.
- Only open new threads for issues that are genuinely new or that remain unresolved.
- If a previous finding was partially addressed, update the thread with what still needs attention rather than opening a duplicate.
12 changes: 7 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ name: CI
'on':
pull_request:
branches:
- test-develop-ci
- test-master-ci
- develop
- master
paths-ignore:
- README.md

Expand All @@ -14,12 +14,14 @@ jobs:

sylius-matrix:
needs: [quality]
if: github.base_ref == 'test-develop-ci'
if: github.base_ref == 'develop'
uses: payplug/template-ci/.github/workflows/sylius_phpunit.yml@main
with:
symfony-versions: '["7.3"]'

sonarcloud:
if: always() && !failure() && !cancelled() && github.base_ref == 'test-develop-ci'
needs: sylius-matrix
if: always() && !failure() && !cancelled() && github.base_ref == 'develop'
needs: [sylius-matrix]
uses: payplug/template-ci/.github/workflows/sonarcloud.yml@main
with:
project-name: 'github-payplug-payplug-syliuspayplugplugin'
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ jobs:
phpunit:
needs: [quality]
uses: payplug/template-ci/.github/workflows/sylius_phpunit.yml@main
with:
symfony-versions: '["7.3"]'

github-release:
needs: phpunit
Expand Down
20 changes: 20 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,26 @@ services:
- name: sylius.payment_request.provider.http_response
gateway_factory: !php/const PayPlug\SyliusPayPlugPlugin\Gateway\ScalapayGatewayFactory::FACTORY_NAME

## Wero Payplug Gateway ##
payplug_sylius_payplug_plugin.command_provider.payplug_wero:
class: Sylius\Bundle\PaymentBundle\CommandProvider\ActionsCommandProvider
arguments:
- !tagged_locator
tag: payplug_sylius_payplug_plugin.command_provider.payplug_wero
index_by: 'action'
tags:
- name: sylius.payment_request.command_provider
gateway_factory: !php/const PayPlug\SyliusPayPlugPlugin\Gateway\WeroGatewayFactory::FACTORY_NAME
payplug_sylius_payplug_plugin.provider.order_pay.http_response.payplug_wero:
class: Sylius\Bundle\PaymentBundle\Provider\ActionsHttpResponseProvider
arguments:
- !tagged_locator
tag: payplug_sylius_payplug_plugin.http_response_provider.payplug_wero
index_by: action
tags:
- name: sylius.payment_request.provider.http_response
gateway_factory: !php/const PayPlug\SyliusPayPlugPlugin\Gateway\WeroGatewayFactory::FACTORY_NAME


## Apple Pay Payplug Gateway ##
payplug_sylius_payplug_plugin.command_provider.payplug_apple_pay:
Expand Down
9 changes: 9 additions & 0 deletions config/services/client.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,14 @@
method="create"/>
<argument type="string" key="$factoryName">payplug_scalapay</argument><!-- Gateway factory name -->
</service>

<service id="payplug_sylius_payplug_plugin.api_client.wero"
class="PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClient"
public="true"
lazy="true">
<factory service="PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory"
method="create"/>
<argument type="string" key="$factoryName">payplug_wero</argument><!-- Gateway factory name -->
</service>
</services>
</container>
8 changes: 8 additions & 0 deletions config/services/gateway.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,13 @@
<tag name="payum.gateway_factory_builder"
factory="payplug_scalapay"/>
</service>

<!-- Gateway Wero by PayPlug -->
<service id="payplug_sylius_payplug_plugin.gateway_factory.wero"
class="Payum\Core\Bridge\Symfony\Builder\GatewayFactoryBuilder">
<argument type="string">PayPlug\SyliusPayPlugPlugin\Gateway\WeroGatewayFactory</argument>
<tag name="payum.gateway_factory_builder"
factory="payplug_wero"/>
</service>
</services>
</container>
6 changes: 6 additions & 0 deletions config/twig_hooks/admin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ sylius_twig_hooks:
'sylius_admin.payment_method.create.content.form.sections.gateway_configuration.payplug_scalapay': &scalapayGateway
live_checkbox: *liveCheckbox

'sylius_admin.payment_method.create.content.form.sections.gateway_configuration.payplug_wero': &weroGateway
live_checkbox: *liveCheckbox

'sylius_admin.payment_method.update.content.form.sections.gateway_configuration.payplug':
<<: *payplugGateway
renew_oauth: &renewOAuth
Expand All @@ -53,3 +56,6 @@ sylius_twig_hooks:
'sylius_admin.payment_method.update.content.form.sections.gateway_configuration.payplug_scalapay':
<<: *scalapayGateway
renew_oauth: *renewOAuth
'sylius_admin.payment_method.update.content.form.sections.gateway_configuration.payplug_wero':
<<: *weroGateway
renew_oauth: *renewOAuth
3 changes: 3 additions & 0 deletions config/twig_hooks/shop.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,6 @@ sylius_twig_hooks:
'sylius_shop.shared.form.select_payment.payment.choice.details#payplug_scalapay':
scalapay:
template: '@PayPlugSyliusPayPlugPlugin/shop/select_payment/_scalapay.html.twig'
'sylius_shop.shared.form.select_payment.payment.choice.details#payplug_wero':
wero:
template: '@PayPlugSyliusPayPlugPlugin/shop/select_payment/_wero.html.twig'
41 changes: 41 additions & 0 deletions public/assets/wero/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions ruleset/phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1366,6 +1366,12 @@ parameters:
count: 1
path: ../src/Resolver/ScalapayPaymentMethodsResolverDecorator.php

-
message: '#^Method PayPlug\\SyliusPayPlugPlugin\\Resolver\\WeroPaymentMethodsResolverDecorator\:\:getSupportedMethods\(\) should return array\<Sylius\\Component\\Payment\\Model\\PaymentMethodInterface\> but returns array\.$#'
identifier: return.type
count: 1
path: ../src/Resolver/WeroPaymentMethodsResolverDecorator.php

-
message: '#^Call to method apply\(\) on an unknown class SM\\StateMachine\\StateMachineInterface\.$#'
identifier: class.notFound
Expand Down
4 changes: 4 additions & 0 deletions src/Command/Provider/CapturePaymentRequestCommandProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
'payplug_sylius_payplug_plugin.command_provider.payplug_scalapay',
['action' => PaymentRequestInterface::ACTION_CAPTURE],
)]
#[AutoconfigureTag(
'payplug_sylius_payplug_plugin.command_provider.payplug_wero',
['action' => PaymentRequestInterface::ACTION_CAPTURE],
)]
final class CapturePaymentRequestCommandProvider implements PaymentRequestCommandProviderInterface
{
public function supports(PaymentRequestInterface $paymentRequest): bool
Expand Down
4 changes: 4 additions & 0 deletions src/Command/Provider/NotifyPaymentRequestCommandProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
'payplug_sylius_payplug_plugin.command_provider.payplug_scalapay',
['action' => PaymentRequestInterface::ACTION_NOTIFY],
)]
#[AutoconfigureTag(
'payplug_sylius_payplug_plugin.command_provider.payplug_wero',
['action' => PaymentRequestInterface::ACTION_NOTIFY],
)]
final class NotifyPaymentRequestCommandProvider implements PaymentRequestCommandProviderInterface
{
public function supports(PaymentRequestInterface $paymentRequest): bool
Expand Down
4 changes: 4 additions & 0 deletions src/Command/Provider/StatusPaymentRequestCommandProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
'payplug_sylius_payplug_plugin.command_provider.payplug_scalapay',
['action' => PaymentRequestInterface::ACTION_STATUS],
)]
#[AutoconfigureTag(
'payplug_sylius_payplug_plugin.command_provider.payplug_wero',
['action' => PaymentRequestInterface::ACTION_STATUS],
)]
final class StatusPaymentRequestCommandProvider implements PaymentRequestCommandProviderInterface
{
public function __construct(private RequestStack $requestStack)
Expand Down
2 changes: 2 additions & 0 deletions src/Creator/PayPlugPaymentDataCreator.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use PayPlug\SyliusPayPlugPlugin\Gateway\OneyGatewayFactory;
use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory;
use PayPlug\SyliusPayPlugPlugin\Gateway\ScalapayGatewayFactory;
use PayPlug\SyliusPayPlugPlugin\Gateway\WeroGatewayFactory;
use Sylius\Component\Core\Model\AddressInterface;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Model\OrderInterface;
Expand Down Expand Up @@ -355,6 +356,7 @@ private function addPaymentMethodFieldToDetails(ArrayObject $details, string $ga
ApplePayGatewayFactory::FACTORY_NAME => ApplePayGatewayFactory::PAYMENT_METHOD_APPLE_PAY,
AmericanExpressGatewayFactory::FACTORY_NAME => AmericanExpressGatewayFactory::PAYMENT_METHOD_AMERICAN_EXPRESS,
ScalapayGatewayFactory::FACTORY_NAME => ScalapayGatewayFactory::PAYMENT_METHOD_SCALAPAY,
WeroGatewayFactory::FACTORY_NAME => WeroGatewayFactory::PAYMENT_METHOD_WERO,
];
// match function is only supported by php 8. so can not use it here.
foreach ($paymentMethods as $name => $method) {
Expand Down
25 changes: 25 additions & 0 deletions src/Gateway/Form/Type/WeroGatewayConfigurationType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace PayPlug\SyliusPayPlugPlugin\Gateway\Form\Type;

use PayPlug\SyliusPayPlugPlugin\Gateway\WeroGatewayFactory;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag(
'sylius.gateway_configuration_type',
[
'type' => 'payplug_wero',
'label' => 'payplug_sylius_payplug_plugin.ui.wero_gateway_label',
'priority' => 90,
],
)]
final class WeroGatewayConfigurationType extends AbstractGatewayConfigurationType
{
protected string $gatewayFactoryTitle = WeroGatewayFactory::FACTORY_TITLE;

protected string $gatewayFactoryName = WeroGatewayFactory::FACTORY_NAME;

protected string $gatewayBaseCurrencyCode = WeroGatewayFactory::BASE_CURRENCY_CODE;
}
14 changes: 14 additions & 0 deletions src/Gateway/WeroGatewayFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace PayPlug\SyliusPayPlugPlugin\Gateway;

final class WeroGatewayFactory extends AbstractGatewayFactory
{
public const FACTORY_NAME = 'payplug_wero';

public const FACTORY_TITLE = 'Wero by PayPlug';

public const PAYMENT_METHOD_WERO = 'wero';
}
Loading
Loading