Payment Method Configuration and Availability: canUseCheckout and isAvailable
Payment Method Configuration and Availability: canUseCheckout and isAvailable
~7 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026
Chapter 62 already set can_use_checkout and can_use_internal as static config values, chapter 63 explained the one case in which the payment method should even become visible. This chapter connects both: the capability flags in detail, and the individual isAvailable() logic that actually checks that one condition.
canUseCheckout and canUseInternal in detail
MethodInterface knows a whole range of can* capabilities - canCapture, canRefund, canVoid and more, mostly irrelevant for a purely discount-based method with no real gateway. Two of them are deliberately set here: can_use_checkout = 1, because the method only makes sense in storefront checkout, and can_use_internal = 0, because admin order creation (Sales > Orders > Create New Order) has no comparable, logged-in customer context the way storefront checkout does - CustomerSession simply doesn't exist there in the same form. The Adapter from chapter 62 reads both values automatically through the default handler of the ValueHandlerPool from payment/mironsoft_loyalty_points/can_use_checkout and .../can_use_internal respectively - not a single line of PHP needed, pure configuration.
isAvailable() and the custom AvailabilityHandler
isAvailable() is a different question than can_use_checkout: it doesn't ask "is this method allowed to appear in checkout at all" but "should it, RIGHT NOW, for THIS cart" - dynamic, per request. The default handler cannot answer that, it only reads static config values. That's exactly why ValueHandlerPool registers a second, custom handler under the field name availability:
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\Model\Payment;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Payment\Gateway\Config\ValueHandlerInterface;
use Magento\Quote\Api\Data\CartInterface;
use Magento\Store\Model\ScopeInterface;
use Mironsoft\Loyalty\Model\Config\LoyaltyConfig;
/**
* Decides whether "Mit Treuepunkten bezahlt" may be offered for the current
* quote. Unlike the static can_use_checkout/can_use_internal flags from
* chapter 62's config.xml, availability depends on live, per-quote data - the
* default ConfigValueHandler cannot answer this, hence a dedicated handler.
*/
class AvailabilityHandler implements ValueHandlerInterface
{
/**
* Config path for the method's own active flag.
*/
private const string XML_PATH_ACTIVE = 'payment/mironsoft_loyalty_points/active';
/**
* @param ScopeConfigInterface $scopeConfig Store-scoped configuration reader
* @param LoyaltyConfig $loyaltyConfig Loyalty module configuration reader
*/
public function __construct(
private readonly ScopeConfigInterface $scopeConfig,
private readonly LoyaltyConfig $loyaltyConfig,
) {
}
/**
* Returns true only if the method is enabled, a customer is logged in, and
* the points already applied to the quote (chapter 63) already cover the
* quote's full grand total - the one edge case chapter 63 identified where
* the points payment method itself, rather than a discount, is used.
*
* @param array $subject Handler subject, contains the "quote" key for availability checks
* @param int|null $storeId Store ID for scoped config reads
* @return bool
*/
public function handle(array $subject, $storeId = null): bool
{
$quote = $subject['quote'] ?? null;
if (!$quote instanceof CartInterface || !$quote->getCustomerId()) {
return false;
}
$isActive = (bool) $this->scopeConfig->getValue(
self::XML_PATH_ACTIVE,
ScopeInterface::SCOPE_STORE,
$storeId
);
if (!$isActive) {
return false;
}
$pointsToRedeem = (int) $quote->getData('loyalty_points_to_redeem');
if ($pointsToRedeem <= 0) {
return false;
}
$coveredAmount = $pointsToRedeem / $this->loyaltyConfig->getPointsPerEuro();
return $coveredAmount >= (float) $quote->getData('grand_total');
}
}
etc/di.xml from chapter 62 gets exactly one addition inside the already-existing ValueHandlerPool virtualType:
<virtualType name="Mironsoft\Loyalty\Model\Payment\ValueHandlerPool"
type="Magento\Payment\Gateway\Config\ValueHandlerPool">
<arguments>
<argument name="handlers" xsi:type="array">
<item name="default" xsi:type="string">Magento\Payment\Gateway\Config\ConfigValueHandler</item>
<item name="availability" xsi:type="string">Mironsoft\Loyalty\Model\Payment\AvailabilityHandler</item>
</argument>
</arguments>
</virtualType>
<!-- rest of etc/di.xml unchanged, see chapter 62 -->
Achtung: AvailabilityHandler::handle() must never throw an exception. The ValueHandlerPool catches nothing here - an unhandled exception while rendering the payment method list would crash the ENTIRE payment step, even for customers who never wanted to use the points payment method in the first place. Chapter 70 revisits this exact mistake.
The admin configuration
For active, title, and sort_order to even be editable in the admin under Stores > Configuration > Sales > Payment Methods, the existing payment section needs a new group:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<section id="payment">
<group id="mironsoft_loyalty_points" translate="label" type="text"
sortOrder="500" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Mironsoft Loyalty - Punkte einlösen</label>
<field id="active" translate="label" type="select" sortOrder="10"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>Aktiviert</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="title" translate="label" type="text" sortOrder="20"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>Titel im Checkout</label>
</field>
<field id="sort_order" translate="label" type="text" sortOrder="30"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>Sortierreihenfolge</label>
</field>
</group>
</section>
</system>
</config>
Tipp: After any change to system.xml or config.xml, bin/cache-clean config is enough - the same rule of thumb as widget.xml (chapter 55) or crontab.xml (chapter 32). A full setup:upgrade isn't needed here as long as no database table is involved; the new quote column and the data patch from chapter 63, however, do need one.
Configured, availability-checked - what's still missing is visibility in checkout itself. Chapter 65 registers the frontend component for that.