Skip to content

PRE-3673: Capture and cancel deferred Hosted Fields payments from the admin order screen - #333

Open
ilajili wants to merge 3 commits into
developfrom
feature/PRE-3673
Open

ilajili wants to merge 3 commits into
developfrom
feature/PRE-3673

Conversation

@ilajili

@ilajili ilajili commented Sep 24, 2026 •

Copy link
Copy Markdown

Description

Lets a merchant capture (in full or partially, possibly several times) and cancel (in full or partially) deferred-capture Hosted Fields payments from the admin order screen, built on UPC's capturePayment() / cancelPayment() (PRE-3670).

  • Authorization only: an HF payment is created with capture=false when deferred capture is enabled. The Sylius payment stays authorized, never paid (synchronous response, webhook or notification).
  • Order screen block "PayPlug — Deferred capture": authorized / captured / cancelled / remaining amounts, capture deadline (warning within 48 h), capture and cancel forms. The native "Complete" button is hidden for these payments.
  • AuthorizedPaymentOperationProcessor: eligibility rules, form lock + version (anti double-click / replay) and Sylius transitions (partial capture → authorized, last capture → completed, full cancellation → cancelled). A UPC refusal records nothing and changes no state.
  • Error messages (FR/EN/IT) for each UPC refusal, including "only one capture allowed" and "partial cancellation not enabled".
  • Webhooks: capture / cancellation confirmations are matched by operationId; an asynchronous failure is recorded and logged at critical.
  • complete transition (cron, merchant listener): captures the remaining amount before completing.
  • Full refund of a deferred payment is based on the captured amount.
  • Admin routes declared in YAML (/admin prefix) + admin role check (ROLE_ADMINISTRATION_ACCESS).
  • Dependency: payplug/unified-plugin-core bumped ^1.1.2 → ^1.1.3.
  • composer.lock: refreshed with a full composer update, not only the UPC bump. The goal is UPC 1.3.0 (floor ^1.1.3); other packages moved with it (Symfony 6.4.x patches, PHPUnit 9.6.37 and others). doctrine/orm is still inside its >=3.5 <3.7 pin, at 3.6.9.
  • Docs: merchant documentation in doc/authorized_payment.md, entry in CHANGELOG.md.

Motivation: Hosted Fields payments had no way to be captured later or partially; the merchant could only rely on the legacy redirected flow, which captures the whole authorized amount at once.

Related issue(s): Closes PRE-3673


Type of Change

  • ✨ New feature (non-breaking change that adds functionality)

Checklist

Code Quality

  • Code is linted and formatted
  • No unnecessary commented-out code or debug logs
  • No hardcoded values (use env variables or config)

Testing

  • Unit tests added / updated

Security & Ops

  • No sensitive data or secrets introduced
  • Logging and error handling are appropriate

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

@ilajili ilajili self-assigned this Sep 24, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

@adumont-payplug adumont-payplug changed the title PRE-3673: Capture and cancel deferred Hosted Fields payments from the… PRE-3673: Capture and cancel deferred Hosted Fields payments from the admin order screen Sep 29, 2026

@adumont-payplug adumont-payplug left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of the whole change (PHPUnit on the touched areas: pass, 14 tests; PHPStan level max and ECS: clean).

Verdict: request changes. Fix the two blocking items, make version mandatory, cap the amount input, and justify or trim composer.lock. Decide on the currency-exponent and Payment::amount points (fix or document).

Missing tests:

  • The flush-fails-after-UPC-accepts path.
  • Two requests racing against a slow flush.
  • A missing version field.
  • Oversized or zero-decimal amounts.
  • The webhook path with an unknown operation id on a deferred payment.

Positives:

  • The UPC 4011 and no-amount cancellation reasoning is documented in the code.
  • A refusal records nothing.
  • Routing is deliberately in YAML under the admin prefix, with a role check and an order/payment ownership check.
  • Async failures are flagged and logged at critical.
  • The FR/EN/IT translations are complete and consistent.

Comment thread src/PaymentProcessing/AuthorizedPaymentOperationProcessor.php
Comment thread src/Action/Admin/AuthorizedPaymentController.php
Comment thread src/Action/Admin/AuthorizedPaymentController.php Outdated
Comment thread src/Action/Admin/AuthorizedPaymentController.php Outdated
$this->assertOperable($payment, $expectedVersion, AuthorizationDetails::OPERATION_CAPTURE);
$this->captureRecorded($payment, $amount);

if (0 === AuthorizationDetails::fromDetails($payment->getDetails())->remainingAmount()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Partial cancellation followed by a capture of the rest completes the payment, but Payment::amount keeps the full amount.

Sylius's order payment-state resolver and RefundPlugin read Payment::amount, so the order can read "paid" for more than was captured. RefundPaymentProcessor correctly refunds the captured amount, but the RefundPlugin refundable total is unaffected. Please verify this, then either lower the payment amount or document and test the behaviour.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept Payment::amount at the authorized amount because Sylius has no notion of a partial capture or void, and lowering it would change what RefundPlugin and the order payment-state resolver see, with a risk for other plugins. The captured amount is tracked in the payment details and RefundPaymentProcessor refunds it. Documented in the processor docblock and doc/authorized_payment.md (commit 8cce4ca).

Comment thread composer.lock
Comment thread src/PaymentProcessing/AuthorizedPaymentOperationProcessor.php
Comment thread src/Upc/AuthorizationOperatorInterface.php
Comment thread src/PaymentProcessing/AuthorizedPaymentOperationProcessor.php
- Flush recorded operation before releasing the lock so
  a second request cannot capture the same money twice
- Warn merchant that money may have moved after an
  unexpected error instead of saying nothing changed
- Require version on capture and cancel requests, a
  missing value now returns a 400
- Cap amount at 9 integer digits so an overflow is
  refused as invalid, not logged as critical
- Convert amounts with UPC AmountHelper
- Align cancel parameters order with capture
- Log only status and execCode when no operationIds
- Document lock TTL and why Payment amount is untouched
Sylius 2.3 drops knp-gaufrette-bundle, which RefundPlugin
still loads, so the PHP 8.4 install step fails. Keep the
matrix on 2.2 until RefundPlugin follows.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants