Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Common Mistakes with Custom Payment and Shipping Methods

Common Mistakes with Custom Payment and Shipping Methods

~7 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026

Block 8 ends the way block 6 did with chapter 54 (accessibility) and block 7 did with chapter 61 (widget vs. content type): a collected look back, this time at the mistakes that actually happen most often while building custom payment and shipping methods.

Silently missing payment method in checkout

Achtung: The single most common mistake, already flagged in chapter 65: the key in the layout XML's renders array doesn't exactly match PointsRedemptionConfigProvider::METHOD_CODE - no JavaScript error, no PHP exception, the method is simply missing from the list. Always check window.checkoutConfig.payment.methods via browser devtools first, to see whether the method code even shows up as available there, before looking at the renderer registration.

Booking in the wrong place

Achtung: collectRates() and the AvailabilityHandler check potentially run multiple times per request - any booking logic there (ledger entry, points balance reduction) would deduct again on every repeated call. The same mistake as with ApplyPointsRedemptionToTotalsPlugin (chapter 39), and the solution chapters 63/67 deliberately chose: book exclusively in sales_order_place_after, guaranteed exactly once per order.

Uncaught exceptions in the availability check

Achtung: If AvailabilityHandler::handle() (chapter 64) throws an unhandled exception - for instance because CustomerRepositoryInterface::getById() fails for a customer that's since been deleted - the ENTIRE payment step breaks, not just this one payment method. The ValueHandlerPool catches nothing here automatically. Defensive type checks (instanceof) and early return false paths instead of deeply nested access are not a style choice here, but robustness.

Confusing the config cache with setup:upgrade

Achtung: A new system.xml group (chapters 64/67) only needs bin/cache-clean config. A new database column (chapter 63's quote.loyalty_points_to_redeem) or a new data patch (chapter 63's InstallSalesLoyaltyRedeemedAttribute), however, absolutely needs bin/magento setup:upgrade. If only the cache gets cleared, the column stays invisible, and $quote->setData('loyalty_points_to_redeem', ...) either fails or gets silently dropped on save.

Forgetting allow_multiple_address

Achtung: payment.xml's allow_multiple_address (chapter 62) is deliberately 0 for mironsoft_loyalty_points: a multi-address order (several shipping addresses, several partial orders out of one cart) has no single, unambiguous grand_total that AvailabilityHandler (chapter 64) could check the redeemed points amount against. Leave the value at its default of 1, and the payment method shows up there without the availability logic ever having been tested for it.

Forgetting country filtering on shipping methods

Achtung: If sallowspecific/specificcountry are missing from system.xml (chapters 66/67), "Free shipping via points" can't be restricted to specific countries in the admin, even if checkAvailableShipCountries() is called correctly in code - the check simply always reads the same, non-editable default value.

Mixing the router with the raw AJAX route

Achtung: The custom Router from chapter 46 specifically intercepts the pretty URLs under /rewards/.... Controller\Ajax\ApplyPoints (chapter 63) deliberately uses the plain, technical default route mironsoft_loyalty/ajax/applypoints and needs no router change for it. Accidentally trying to "prettify" this route through the router too breaks the AJAX call from chapter 65 - form/AJAX targets aren't SEO-relevant, the same reasoning already used for the redeem form in chapter 53.

Tipp: That wraps up block 8: a payment method built on the modern Adapter facade, a deliberately discount-based solution for partial payment instead of real split payment, an explicitly justified, tightly scoped Knockout.js exception in the checkout frontend, and a shipping method built on the classic AbstractCarrier path - both booked through the same, idempotency-conscious sales_order_place_after observer. Block 9 turns to a custom product type next.