Skip to content

PRE-3472: Fix error 500 when user proceed to return_cancel action on scalapay checkout#304

Open
jhoaraupp wants to merge 1 commit into
release/2.2.0from
fix/PRE-3472
Open

PRE-3472: Fix error 500 when user proceed to return_cancel action on scalapay checkout#304
jhoaraupp wants to merge 1 commit into
release/2.2.0from
fix/PRE-3472

Conversation

@jhoaraupp

Copy link
Copy Markdown

Description

PRE-3472: Fix error 500 when user proceed to return_cancel action on scalapay checkout

Motivation:

Related issue(s): Closes #


Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue) [x]
  • ✨ New feature (non-breaking change that adds functionality) [ ]
  • 💥 Breaking change (fix or feature that causes existing functionality to change and that could impact other libs) [ ]
  • 🔧 Refactor (no functional changes, code improvement only) [ ]
  • 📦 Dependency update [ ]
  • 🔒 Security fix [ ]
  • 📝 Documentation update [ ]

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

Copilot AI review requested due to automatic review settings July 1, 2026 13:53

Copilot AI 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.

Pull request overview

This PR aims to prevent a 500 error during Scalapay “return_cancel” by making payment state transitions idempotent (checking can() before apply()), so already-transitioned payments don’t trigger invalid-transition exceptions.

Changes:

  • Refactor payment status → transition mapping to compute a $transition first in both handlers.
  • Guard state-machine transitions with StateMachineInterface::can() before calling apply().
  • Add warning log + early return in the status handler when a transition can’t be applied.

1. What's Good

  • Prevents invalid state-machine transition exceptions by using can() checks before apply().
  • Adds structured context to the Status handler log (payment id, current state, transition, status), which helps diagnose edge cases.
  • Keeps the status→transition mapping centralized and easy to scan via match.

2. Summary table

Dimension Rating
Security ✅ Fine
Correctness ⚠️ Medium (forced status may persist even when transition is skipped)
Performance ✅ Fine
Maintainability ⚠️ Low (inconsistent observability between handlers)

3. Closing one-liner

Address the forced-status persistence edge case (and improve observability when transitions are skipped) before merge.


4. Individual findings

Heading: Correctness ⚠️ Medium
Subtitle (bold): Forced status can persist even when transition is skipped (StatusPaymentRequestHandler.php:109)
Code block:

if (!$this->stateMachine->can($payment, PaymentTransitions::GRAPH, $transition)) {
    $this->logger->warning('[PayPlug] Cannot apply payment transition, payment is already in an incompatible state.', [
        'payment_id' => $payment->getId(),
        'current_state' => $payment->getState(),
        'transition' => $transition,
        'status' => $payment->getDetails()['status'] ?? '',
    ]);

    return;
}

Explanation paragraph: With forced status coming from the status query parameter, this early return prevents the exception (good), but it can also allow an overwritten details['status'] (e.g. canceled) to be persisted even though the Payment state remains completed. That creates a “details vs state” inconsistency that can confuse later logic that relies on details['status'].
Fix: Ensure forced status is only persisted when the transition is actually applied (e.g. move the forced-status write behind the can() check, or have updatePaymentState() return whether it applied and revert/skip the forced status when it didn’t).

Heading: Operational implications ⚠️ Low
Subtitle (bold): Log message is misleading when can() is false for idempotent calls (StatusPaymentRequestHandler.php:110)
Code block:

$this->logger->warning('[PayPlug] Cannot apply payment transition, payment is already in an incompatible state.', [

Explanation paragraph: can() can be false both for truly incompatible states and for “already applied” / idempotent scenarios; the current message only mentions incompatibility, which can cause unnecessary alerting/confusion.
Fix: Adjust the message to cover both cases.

$this->logger->warning('[PayPlug] Cannot apply payment transition (already applied or incompatible with current state).', [

Heading: Operational implications ⚠️ Low
Subtitle (bold): Skipped transitions become non-observable in Notify handler (NotifyPaymentRequestHandler.php:99)
Code block:

if ($this->stateMachine->can($payment, PaymentTransitions::GRAPH, $transition)) {
    $this->stateMachine->apply($payment, PaymentTransitions::GRAPH, $transition);
}

Explanation paragraph: This avoids exceptions (good), but it also removes the previous signal (exception → failed PaymentRequest) when transitions can’t be applied for unexpected reasons. Without logging, real data/config issues may go unnoticed while requests still complete.
Fix: Log when can() is false (inject LoggerInterface and log payment id/state/transition/status when skipping).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/Command/Handler/StatusPaymentRequestHandler.php Adds can() guard + warning log around payment state transitions to avoid invalid-transition 500s.
src/Command/Handler/NotifyPaymentRequestHandler.php Adds can() guard around payment state transitions to make notification handling idempotent.

Comment thread src/Command/Handler/StatusPaymentRequestHandler.php Outdated
Comment thread src/Command/Handler/StatusPaymentRequestHandler.php Outdated
Comment thread src/Command/Handler/NotifyPaymentRequestHandler.php Outdated

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread src/Command/Handler/StatusPaymentRequestHandler.php Outdated
Comment thread src/Command/Handler/StatusPaymentRequestHandler.php Outdated
Comment thread src/Command/Handler/NotifyPaymentRequestHandler.php Outdated

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

PayPlugApiClientInterface::FAILED => $this->stateMachine
->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_FAIL),
default => throw new \LogicException(sprintf('Unknown payment status "%s".', $payment->getDetails()['status'] ?? '')), // @phpstan-ignore-line - getDetails() return mixed
$transition = match ($payment->getDetails()['status'] ?? '') { // @phpstan-ignore-line - getDetails() return mixed

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.

Maybe we could do a TransitionHelper with this to refacto those two files with this big duplica

Copilot AI review requested due to automatic review settings July 2, 2026 15:45

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 3 changed files in this pull request and generated 4 comments.

Comment on lines 104 to 110
$transition = match ($payment->getDetails()['status'] ?? '') { // @phpstan-ignore-line - getDetails() return mixed
PayPlugApiClientInterface::STATUS_ABORTED, PayPlugApiClientInterface::STATUS_CANCELED, PayPlugApiClientInterface::STATUS_CANCELED_BY_ONEY => PaymentTransitions::TRANSITION_CANCEL,
PayPlugApiClientInterface::STATUS_AUTHORIZED => PaymentTransitions::TRANSITION_AUTHORIZE,
PayPlugApiClientInterface::STATUS_CAPTURED => PaymentTransitions::TRANSITION_COMPLETE,
PayPlugApiClientInterface::FAILED => PaymentTransitions::TRANSITION_FAIL,
default => null,
};
Comment on lines 93 to 99
$transition = match ($payment->getDetails()['status'] ?? '') { // @phpstan-ignore-line - getDetails() return mixed
PayPlugApiClientInterface::STATUS_ABORTED, PayPlugApiClientInterface::STATUS_CANCELED, PayPlugApiClientInterface::STATUS_CANCELED_BY_ONEY => PaymentTransitions::TRANSITION_CANCEL,
PayPlugApiClientInterface::STATUS_AUTHORIZED => PaymentTransitions::TRANSITION_AUTHORIZE,
PayPlugApiClientInterface::STATUS_CAPTURED => PaymentTransitions::TRANSITION_COMPLETE,
PayPlugApiClientInterface::FAILED => PaymentTransitions::TRANSITION_FAIL,
default => null,
};
Comment on lines +102 to +104
private function updatePaymentState(PaymentInterface $payment): bool
{
match ($payment->getDetails()['status'] ?? '') {
PayPlugApiClientInterface::STATUS_ABORTED, PayPlugApiClientInterface::STATUS_CANCELED, PayPlugApiClientInterface::STATUS_CANCELED_BY_ONEY => $this->stateMachine
->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CANCEL),
PayPlugApiClientInterface::STATUS_AUTHORIZED => $this->stateMachine
->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_AUTHORIZE),
PayPlugApiClientInterface::STATUS_CAPTURED => $this->stateMachine
->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_COMPLETE),
PayPlugApiClientInterface::FAILED => $this->stateMachine
->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_FAIL),
default => throw new \LogicException(sprintf('Unknown payment status "%s".', $payment->getDetails()['status'] ?? '')), // @phpstan-ignore-line - getDetails() return mixed
$transition = match ($payment->getDetails()['status'] ?? '') { // @phpstan-ignore-line - getDetails() return mixed
Comment on lines +91 to +93
private function updatePaymentState(PaymentInterface $payment): void
{
match ($payment->getDetails()['status'] ?? '') {
PayPlugApiClientInterface::STATUS_ABORTED, PayPlugApiClientInterface::STATUS_CANCELED, PayPlugApiClientInterface::STATUS_CANCELED_BY_ONEY => $this->stateMachine
->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CANCEL),
PayPlugApiClientInterface::STATUS_AUTHORIZED => $this->stateMachine
->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_AUTHORIZE),
PayPlugApiClientInterface::STATUS_CAPTURED => $this->stateMachine
->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_COMPLETE),
PayPlugApiClientInterface::FAILED => $this->stateMachine
->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_FAIL),
default => throw new \LogicException(sprintf('Unknown payment status "%s".', $payment->getDetails()['status'] ?? '')), // @phpstan-ignore-line - getDetails() return mixed
$transition = match ($payment->getDetails()['status'] ?? '') { // @phpstan-ignore-line - getDetails() return mixed

Copilot AI 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.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.

Comment on lines +106 to +108
self::assertFalse($this->applier->apply($payment));
}
}
Comment on lines +23 to +47
$transition = match ($payment->getDetails()['status'] ?? '') { // @phpstan-ignore-line - getDetails() return mixed
PayPlugApiClientInterface::STATUS_ABORTED, PayPlugApiClientInterface::STATUS_CANCELED, PayPlugApiClientInterface::STATUS_CANCELED_BY_ONEY => PaymentTransitions::TRANSITION_CANCEL,
PayPlugApiClientInterface::STATUS_AUTHORIZED => PaymentTransitions::TRANSITION_AUTHORIZE,
PayPlugApiClientInterface::STATUS_CAPTURED => PaymentTransitions::TRANSITION_COMPLETE,
PayPlugApiClientInterface::FAILED => PaymentTransitions::TRANSITION_FAIL,
default => null,
};

if (null === $transition) {
$this->logger->warning('[PayPlug] Cannot apply payment transition: unknown status.', [
'sylius_payment_id' => $payment->getId(),
'payplug_payment_id' => $payment->getDetails()['payment_id'] ?? null,
'status' => $payment->getDetails()['status'] ?? '',
]);

return false;
}

if (!$this->stateMachine->can($payment, PaymentTransitions::GRAPH, $transition)) {
$this->logger->warning('[PayPlug] Cannot apply payment transition (already applied or incompatible with current state).', [
'sylius_payment_id' => $payment->getId(),
'payplug_payment_id' => $payment->getDetails()['payment_id'] ?? null,
'current_state' => $payment->getState(),
'transition' => $transition,
'status' => $payment->getDetails()['status'] ?? '',
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.

3 participants