Send a payment link to the customer from the admin order view - #126
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 2.x #126 +/- ##
============================================
+ Coverage 85.99% 87.37% +1.37%
- Complexity 154 182 +28
============================================
Files 20 23 +3
Lines 614 689 +75
============================================
+ Hits 528 602 +74
- Misses 86 87 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
425a87f to
7fe838a
Compare
bf9aaeb to
6877f47
Compare
7fe838a to
f98a7a3
Compare
| public function getFunctions(): array | ||
| { | ||
| return [ | ||
| new TwigFunction('setono_sylius_quickpay_payment_link', $this->paymentLink(...)), |
There was a problem hiding this comment.
Done — back to Twig runtimes (PaymentLinkRuntime here, and PaymentMethodLogoRuntime in #125 for consistency), registered with twig.runtime.
For context on why they had been folded into the extensions: with twig/twig ^2.15 || ^3.0, the lowest-deps job resolves Twig 2.15, whose TwigFunction constructor docblock is callable|null — the runtime form [SomeRuntime::class, 'method'] isn't a PHP callable, so PHPStan (level max) rejected it there. Twig only documents callable|array{class-string, string}|null from 3.9. Rather than raise the plugin's Twig floor over a docblock, phpstan.neon (in #125) now ignores exactly that error under src/Twig/* with an explanatory comment — reportUnmatchedIgnoredErrors is already false, so it's silent on the highest-deps side. If you'd rather bump to twig/twig ^3.9 instead, say so and I'll swap.
| if ($container->hasExtension('sylius_mailer')) { | ||
| $container->prependExtensionConfig('sylius_mailer', [ | ||
| 'emails' => [ | ||
| SendPaymentLinkAction::EMAIL_CODE => [ |
There was a problem hiding this comment.
These codes are usually not defined on controller actions. Check what Sylius does
There was a problem hiding this comment.
Done. Sylius keeps them in an interface of constants (Sylius\Bundle\CoreBundle\Mailer\Emails) and sends through email managers (OrderEmailManager etc.), so this mirrors that: Mailer\Emails::PAYMENT_LINK holds the code (used by the sylius_mailer prepend), and a new Mailer\PaymentLinkEmailManager (sendPaymentLinkEmail(PaymentInterface $payment, string $paymentLink)) wraps sylius.email_sender — the controller now only resolves the link, delegates to the manager and flashes. Both are unit-tested.
|
|
||
| // Sylius pays the order's last payment awaiting payment, so that is the only one the link | ||
| // can be for; anything else would show a link that pays a different payment | ||
| if ($order->getLastPayment(PaymentInterface::STATE_NEW) !== $payment) { |
There was a problem hiding this comment.
What if an order has multiple 'new' payments, but the quickpay one is not the last one?
There was a problem hiding this comment.
Then no link is shown for the Quickpay payment — deliberately, and I've expanded the comment to say why.
The link is Sylius' sylius_shop_order_pay, and PayumController::prepareCaptureAction always charges $order->getLastPayment(PaymentInterface::STATE_NEW) — the plugin can't point it at another payment. If we showed the link on an earlier new Quickpay payment, the customer would end up paying the later one, possibly through another gateway entirely. So the panel is only offered on the payment Sylius will actually charge; if that last payment is a Quickpay one, the panel shows there (which is what the test "an earlier payment when a later one awaits payment" pins down).
It's also an edge case Sylius core doesn't produce: OrderPaymentProcessor reuses the last new payment (updates amount/currency) rather than adding a second one, so two new payments only arise in customized setups — and there the link still belongs to whatever payment Sylius would charge, Quickpay or not. Happy to hear if you see a scenario where hiding it is the wrong call.
6877f47 to
d36afdf
Compare
f98a7a3 to
76ca224
Compare
| } | ||
|
|
||
| $session = $request->getSession(); | ||
| \assert($session instanceof Session); |
There was a problem hiding this comment.
Don't use \assert. Instead check if $session actually is what you expect
There was a problem hiding this comment.
Done — no more \assert. The action now takes the RequestStack and gets the flash bag through Sylius' own FlashBagProvider::getFlashBag($this->requestStack), exactly as ResendOrderConfirmationEmailAction does (it checks the bag is a FlashBagInterface and fails loudly otherwise).
| use Setono\SyliusQuickpayPlugin\Twig\PaymentLinkRuntime; | ||
| use Sylius\Component\Core\Model\PaymentInterface; | ||
|
|
||
| final class PaymentLinkRuntimeTest extends TestCase |
There was a problem hiding this comment.
Use \Twig\Test\IntegrationTestCase to test the Twig extension
There was a problem hiding this comment.
Done — tests/Twig/PaymentLinkExtensionTest now extends Twig\Test\IntegrationTestCase: it builds a real Twig environment with the extension, a FactoryRuntimeLoader for PaymentLinkRuntime, and the real PaymentLinkProvider on top of a real Symfony UrlGenerator; the cases live in Fixtures/PaymentLink/payment_link.test (a Quickpay payment awaiting payment → the channel-hostname pay url; a completed one → nothing). Same treatment for the logo extension in #125. The test class implements both getFixturesDir() (Twig < 3.13 — the lowest supported is 2.15) and getFixturesDirectory(), so it runs across the matrix.
| <form action="{{ path('setono_sylius_quickpay_admin_send_payment_link', {'id': payment.id}) }}" method="post" style="margin-top: .75em;"> | ||
| <input type="hidden" name="_csrf_token" value="{{ csrf_token(payment.id) }}"> | ||
| <button type="submit" class="ui icon labeled tiny fluid button"> | ||
| <i class="send icon"></i> {{ 'setono_sylius_quickpay.ui.send_payment_link'|trans({'%email%': payment.order.customer.email|default('')}) }} | ||
| </button> | ||
| </form> |
There was a problem hiding this comment.
Is this how Sylius does it? Sylius has the resend confirmation email functionality. Please verify
There was a problem hiding this comment.
Verified against Sylius' resend-order-confirmation-email and aligned to it. Sylius renders _resendEmail.html.twig as an <a class="ui icon labeled tiny fluid button"> whose href is a GET route with the csrf token as a query parameter (path('sylius_admin_order_resend_confirmation_email', {'id': order.id, '_csrf_token': csrf_token(order.id)}); route methods: [GET]), and the action reads $request->query->get('_csrf_token'), flashes and redirects. The send button is now exactly that shape (link, not a form): route setono_sylius_quickpay_admin_send_payment_link is GET /admin/quickpay/payments/{id}/send-payment-link with _csrf_token in the query, token id = payment id, sylius_test_html_attribute('send-quickpay-payment-link') on the link. Clicked through it on the running test app after the change — flash + redirect as before.
d36afdf to
d0bd832
Compare
9db425d to
b8f83bb
Compare
Every Quickpay payment still awaiting payment now shows a Payment link panel on the admin order view: a copy-able link and a button that emails it to the customer — for phone or invoice orders, or after an abandoned checkout. The link is Sylius' own sylius_shop_order_pay url built for the order's channel hostname, deliberately not a raw Quickpay window url: opening it mints a fresh Payum token and runs the normal Convert → payment-window flow, so nothing happens at Quickpay until the customer clicks, it can be shown and sent as often as needed, and a payment whose Quickpay payment was never created works too. PaymentLinkProvider offers it for the order's last payment in state new on a Quickpay method of a non-cancelled order — the payment Sylius' own "Pay" button pays — and null otherwise (including a headless shop without the route). SendPaymentLinkAction (POST, CSRF token = payment id as in Sylius' admin) emails email/payment_link.html.twig through sylius.email_sender in the order's locale and flashes the outcome. Translations for the panel, the email and the flashes in all 16 locales; sylius/mailer-bundle, symfony/routing and symfony/security-csrf are now declared since the plugin uses them directly. Closes #107
One plugin, one Twig extension: setono_sylius_quickpay_payment_method_logos and setono_sylius_quickpay_payment_link now live on Twig/PaymentExtension, backed by a single Twig/PaymentRuntime carrying both providers, and are covered by one IntegrationTestCase running both fixtures.
b8f83bb to
7116f3c
Compare
Stacked on #125.
Every Quickpay payment still awaiting payment shows a Payment link panel on the admin order view: a copy-able link and a button that emails it to the customer — for phone/invoice orders or after an abandoned checkout.
Design note: the link is Sylius' own
sylius_shop_order_payurl (built for the order's channel hostname), deliberately not a raw Quickpay window url. Opening it mints a fresh Payum token and runs the normal Convert → payment-window flow, so nothing happens at Quickpay until the customer clicks, it can be shown/copied/sent as often as needed, and it also works for a payment whose Quickpay payment was never created (the flow creates it on first use).PaymentLinkProvideroffers it for the order's last payment in statenewon a Quickpay method of a non-cancelled order — exactly the payment Sylius' own "Pay" button in the customer account pays — and null otherwise (incl. headless shops without the route).SendPaymentLinkAction: POST, CSRF token = payment id (Sylius admin convention), sendsemail/payment_link.html.twigviasylius.email_senderin the order's locale, flashes the outcome (flashesdomain)sylius/mailer-bundle,symfony/routing,symfony/security-csrf, which the plugin now uses directly100 tests, PHPStan max, ECS, Rector, normalize, dependency analyser green.
Closes #107