Repository navigation
Port Locked Payment Methods to the v2 framework - #24
Merged
Merged
Conversation
4 of 10 tasks
Refresh the root's, Linked Orders' and Internal Comments' Composer locks, which move only roave/security-advisories, and their npm locks, which take Babel 7.29.11, ignore 7.0.14, http-proxy-middleware 4.2.1 and their patch-level neighbours, every change upward. No code change was needed. Assisted-by: Claude Code:claude-opus-5-5
Linked Orders and Internal Comments load includes/safeguards.php after the bootstrap package's floor gate, as the template did, so on a runtime below a floor their restrictions would never load while a failed boot keeps them. The template now loads the safeguards first. Each plugin's PluginBootTest gains the template's row, which expects includes/safeguards.php among the included files before the bootstrap package's load.php, and the include-loader row drops its safeguards.php assertion, which the order row covers. Both fail until the entries move. Assisted-by: Claude Code:claude-opus-5-5
Linked Orders and Internal Comments loaded includes/safeguards.php after Requirements::check_floors(), as the template did, so a site below a floor would lose a safeguard's restriction while a failed boot keeps it. Both safeguards files are empty today, but a restriction either plugin adds must hold whenever the plugin cannot boot. Each entry now requires the safeguards first, as the template's does. The template's accompanying changes come along, since each plugin carries its own copy: the AGENTS.md layout lines and safeguards convention, docs/ARCHITECTURE.md's load order, the safeguards file's docblock, and phpstan.safeguards.neon, which lint:php:phpstan runs at PHP 7.4 and WordPress 5.2 and the release zip leaves out. Linked Orders' convention names the template's recipes, as Internal Comments' does, since it ships no docs/recipes.md. X ruling 89. Assisted-by: Claude Code:claude-opus-5-5
Locked Payment Methods for WooCommerce is the third plugin ported to v2. It starts from wordpress-plugin-template's trunk (321d243) through the template's own fill-in, run in a clone of the template: name Locked Payment Methods for WooCommerce, slug locked-payment-methods-for-woocommerce, namespace DeepWebSolutions\LockedPaymentMethods, prefix dws_lpmwc, ports 8826 and 8827, repository ahegyes/wc-locked-payment-methods. As the v1 successor it stays at 2.0.0, which the fill-in sets to 0.1.0 for a new plugin, and its Composer name is its mirror, ahegyes/wc-locked-payment-methods. The directory keeps the template's files but for CONTRIBUTING.md, the pull-request and issue templates, Dependabot's configuration and the fill-in's workflow, script and Quality job, which belong to the monorepo. The template's demo stays until the port replaces it, and every suite passes as filled in. X rulings 87 and 88. Assisted-by: Claude Code:claude-opus-5-5
The fill-in names the Composer package after the plugin's slug, ahegyes/locked-payment-methods-for-woocommerce, while a plugin in this monorepo takes its mirror's name, which the split reads to find the mirror: ahegyes/wc-locked-payment-methods. The scaffold's commit said so but left the fill-in's name; the lock's content hash follows. Assisted-by: Claude Code:claude-opus-5-5
Locked Payment Methods carried the template's demo, which a plugin replaces: the greeting feature, the example block and the editor script, with their options, settings fields, tests and build entries. They go as the template's README describes. v1 shipped no scripts or styles, and the port renders every screen through WordPress and WooCommerce, so nothing is built: blocks/, assets/, the asset registrars, the empty models/ and templates/ extension points, and the block and React packages go; build stays and runs nothing, as the README asks, and lint:styles keeps one pattern with --allow-empty-input. The entry's build gate checks only the bootstrap package, which the partial-deploy row now removes. The data consent and the template's generic readers stay until the port replaces them; the docs follow in the commit that describes the plugin. Assisted-by: Claude Code:claude-opus-5-5
v1 kept its settings in an unlisted section of WooCommerce's Payments tab, under three key spellings, and offered only enabled gateways, so saving the section while a locked gateway was disabled unlocked it once it was enabled again. The port moves them to a listed section on the Advanced tab under the plugin's prefix, and copies v1's values. SettingsTest expects the section on the Advanced tab under v1's id, v1's General and Plugin groups with their defaults, every registered gateway in the locked list with the disabled ones marked and kept through a save, every role in the roles field whether or not the role strategy is on, checkboxes storing 'yes' and 'no', v1's group filters firing as deprecated on v1's field ids, and the Settings and support links. MigrationsTest expects the first step to store every setting, autoloaded, from v1's row where it holds a value of the right kind and otherwise from the default, to leave v1's rows and the values a site stored, to map v1's eight settings one to one, and to throw when a write fails. X ruling 93. Assisted-by: Claude Code:claude-opus-5-5
v1 kept its settings in an unlisted section of the Payments tab, which merchants could not find from WooCommerce's menus, and its locked list offered only enabled gateways, so a save while a locked gateway was disabled dropped it, and enabling the gateway again showed it to every customer. The section moves to the Advanced tab under v1's id, beside Linked Orders', with v1's General, Integrations and Plugin blocks; every setting is a flat option under dws_lpmwc_, and the on/off selects are checkboxes. The locked list offers every registered gateway, marking the disabled ones, so a disabled gateway stays locked, and the roles field always shows. v1's group-field filters fire as deprecated on v1's field ids, and a field one adds keeps the option v1 stored it under. The first migration step copies each of v1's eight settings where its row holds a value of the setting's kind, takes the default otherwise, and leaves v1's rows, so a downgrade still locks what v1 locked. The plugin row links to the section and the support forum. The integration switches arrive with their strategies. X ruling 93. Assisted-by: Claude Code:claude-opus-5-5
v1's lock existed only once v1 had booted, and it removed a gateway only on an exact true, so a site below v1's floors, a failed boot or a callback returning null showed every locked method to every customer. v1 also judged order grants by the order-pay query variable alone, so a checkout request naming another order's id took that order's grants. SafeguardsTest expects the lock to remove the methods the plugin's option lists, unlocking one only on an explicit false from should_lock once the plugin has booted; to read v1's list while the plugin's option is absent, and to stop once the plugin stores its own, an empty one included (X ruling 102); to lock only the strings of a stored list; to let the locked_ids filter and v1's list filters add methods but never unlock stored ones; to fire v1's list and lock filters as deprecated, in v1's order and under v1's safe id; to judge the current user, and an order-pay page's order only given that order's key; and, in fresh requests below the PHP floor and below WooCommerce's, to hold with a relaxation hooked. The request helper moves into a trait both PartialDeployTest and SafeguardsTest use. X rulings 89, 94 and 102. Assisted-by: Claude Code:claude-opus-5-5
v1's lock existed only once v1 had booted and removed a gateway only on an exact true, so a site below v1's floors, a failed boot or a callback returning null showed every locked method to every customer; it also took the order-pay query variable as the order being paid, so a checkout request naming another order's id took that order's grants. The lock is the plugin's safeguard. includes/safeguards.php filters woocommerce_available_payment_gateways at 999 and removes every offered gateway on the plugin's list, or on v1's while the plugin has stored none, since the migration that copies v1's list runs only once the plugin boots (X ruling 102). dws_lpmwc/payment_method/locked_ids and v1's list filters only add ids, so nothing but an explicit false from dws_lpmwc/payment_method/should_lock, once the plugin has booted, unlocks a method. should_lock judges the current user and the order the order-pay page pays, which counts only with that order's key, as WooCommerce requires before it takes the payment. v1's list, value and lock filters fire as deprecated where v1 fired them, the per-id one under v1's safe id, for the offered gateways. phpstan.safeguards.neon now scans WooCommerce's stubs and is the safeguards' only profile, since an ignore WPCompat needs at WordPress 5.2 matches nothing at the plugin's own floor. X rulings 89, 94 and 102. Assisted-by: Claude Code:claude-opus-5-5
v1 granted its capabilities from a permissions component that checked users at boot, and its free build granted the order capability that only its premium build used. CapabilitiesTest expects v1's four names and v1's grants: every capability to administrators, and the user-profile and order ones to shop managers. X ruling 92. Assisted-by: Claude Code:claude-opus-5-5
v1 granted its four capabilities from a permissions component that checked users at boot, and removed them only on uninstall. includes/capabilities.php names them as v1 did and declares v1's grants, which core's CapabilityGrants applies whenever the declaration changes or a declared role appears: every capability to administrators, and the user-profile and order ones to shop managers. Each form checks its capability in its callback, so nothing checks a user at boot. X ruling 92. Assisted-by: Claude Code:claude-opus-5-5
v1's role strategy unlocked every locked method for every visitor, guests included, while its roles list was empty, because v1's role helper answers true for an empty list before it looks the user up. UserRoleStrategyTest expects the strategy to load while its switch is on, as v1's was by default; to unlock every locked method only for a user holding a full-access role, never for a guest and never through an empty list; to leave a method another callback unlocked unlocked; to open the lock at checkout for such a user only; and to read the roles through v1's General value filter, fired as deprecated. X rulings 90 and 94. Assisted-by: Claude Code:claude-opus-5-5
v1's role strategy unlocked every locked method for every visitor, guests included, while its roles list was empty, because v1's role helper answers true for an empty list before it looks the user up. UserRoleStrategy is a component gated on its switch, on by default as v1's was. On dws_lpmwc/payment_method/should_lock it returns false for a user holding one of the full-access roles and leaves every other answer alone, so a guest or an empty list unlocks nothing. It reads the roles through v1's General value filter, which fires as deprecated and now lives beside the safeguard's own read of the locked list in includes/safeguards.php, the one file both can call. The main PHPStan profile scans that file, which it no longer analyzes. X rulings 90 and 94. Assisted-by: Claude Code:claude-opus-5-5
v1 stored a user's grants as global user meta, one row per gateway, so a grant on one store of a network applied on every store; its Users column showed icons without text, its filter printed unescaped markup and read only the top dropdown, and its profile section saved nothing for a method outside the locked list. UserProfileStrategyTest expects the strategy to load while its switch is on, as v1's was by default; to unlock only the methods a user's per-site list holds, ignoring the global key; to offer on the profile the locked methods and the user's other grants, marking a grant outside the list and a method another rule already unlocks; to show nothing without the capability or anything to offer; to store the checked methods and keep the grants a save names wrongly; and to list and filter users by their grants in text, from either dropdown. MigrationsTest expects the grants step to move v1's 'yes' rows into the site's list beside its own grants, to delete every v1 grant row, to converge when run again, and to throw, keeping v1's rows, when a write fails. X ruling 90. Assisted-by: Claude Code:claude-opus-5-5
v1 stored a user's grants as global user meta, one row per gateway, so a grant on one store of a network applied on every store; its Users column showed icons without text, its filter printed unescaped markup and read only the top dropdown, and its profile section saved nothing for a method outside the locked list. A user's grants are one per-site user option, the list of unlocked payment method ids under the site's table prefix, read from that key alone. UserProfileStrategy, gated on its switch, returns false on should_lock for a granted method, and edits the list on the profile through the framework's UserProfile host and an ObjectForm with one checkboxes field, behind unlock_dws_lpm_via_user_profiles and edit_user. The field offers the locked methods and the user's other grants, since a save keeps only offered choices, and marks a grant outside the list and a method another rule already unlocks. The Users screen gains a column naming each locked method's state in text and a filter by grant, read from the dropdown whose button submitted the list. The second migration step moves v1's 'yes' rows into every site's list beside its own grants, keeping v1's reach, then deletes every v1 grant row; a partial run converges on the next. X ruling 90. Assisted-by: Claude Code:claude-opus-5-5
v1's premium build stored order grants as one post meta row per gateway, edited them only on the posts screen, and deleted a disabled control's grant on every save, so saving a paid order or one whose customer was unlocked another way dropped its grants. OrderStrategyTest expects the strategy to load only while its switch is on, as v1's was off by default, and the booted plugin to run it then; on each order storage, an order to unlock only the methods its list holds when it is the order being paid, and a save of its box to store the checked methods; the box to offer the locked methods and the order's other grants, behind unlock_dws_lpm_via_orders, and to be disabled on a paid order, so a save leaves its grants; the order-pay page to unlock the order's methods given its key; and, on each storage, the grants step to move v1's 'yes' rows into the order's list beside its own grants and delete every v1 row. OrderStorage comes from Linked Orders' tests. X rulings 91 and 92. Assisted-by: Claude Code:claude-opus-5-5
v1's premium build stored order grants as one post meta row per gateway, edited them only on the posts screen, and deleted a disabled control's grant on every save, so saving a paid order or one whose customer was unlocked another way dropped its grants. An order's grants are one order meta list, read through WooCommerce's order data on either storage. OrderStrategy, gated on its switch and off by default as v1's was, returns false on should_lock for a method on the list of the order being paid, which the safeguard names only on the order's own payment page given its key. It edits the list in a box on the order screen through the framework's OrderMetaBox, behind unlock_dws_lpm_via_orders and edit_post; a paid order's box is disabled, so a save leaves its grants. The third migration step moves v1's 'yes' rows, found in either storage's meta table, into each order's list beside its own grants, and deletes the order's v1 rows. The profile and order forms build their choices and titles through two shared functions in includes/grants.php. X rulings 91 and 92. Assisted-by: Claude Code:claude-opus-5-5
v1's premium build kept each Groups group's grants in an option of its own, which deleting the group left behind. GroupsStrategyTest, against Groups 4.8.0 and in the groups group so environments without Groups leave it out, expects the strategy to load only while Groups is active and its switch is on; the settings to show the switch while Groups is active; a member of a group or of one of its subgroups to be unlocked for the group's methods; Groups' add and edit forms to offer the locked methods to a user holding both unlock_dws_lpm_via_groups and Groups' own capability, and their saves to store the checked methods under the group, keep them on a choice the field does not offer, and store nothing without the capability or the field; deleting a group to forget its methods; and the grants step to move v1's per-group options into one map and delete them. SettingsTest's integrations rows follow whether Groups is active. X rulings 91 and 92. Assisted-by: Claude Code:claude-opus-5-5
v1's premium build kept each Groups group's grants in an option of its own, which deleting the group left behind. The groups' grants are one option mapping each group's id to its list. GroupsStrategy, gated on Groups at v1's oldest version and on its switch, off by default, returns false on should_lock for a method a group of the user, or a group above one, unlocks. Groups verifies its forms' nonce and its own capability before it fires the actions that save them, and its add form has no group to bind a nonce to, so the field rides Groups' add and edit forms: it renders and sanitizes through the framework's Forms, for a user who also holds unlock_dws_lpm_via_groups, reports a rejection as an admin notice, and a deleted group's entry goes. The settings show the Groups switch while Groups is active. The fourth migration step moves v1's per-group options into the map and deletes them. Groups 4.8.0 comes from WP Packages as a development package, mapped and activated in Locked Payment Methods' own environments; PHPStan scans its classes. X rulings 91 and 92. Assisted-by: Claude Code:claude-opus-5-5
WooCommerce Memberships plans take the same meta key as orders, each on its own object, so the constant that named it for orders alone names it for both. Assisted-by: Claude Code:claude-opus-5-5
v1's premium build kept a WooCommerce Memberships plan's grants in plan meta under v1's prefix and saved its plan tab without a nonce of its own. WooCommerceMembershipsStrategyTest, against a double of Memberships' one function and in the wc-memberships group, since Memberships is a paid plugin absent from WP Packages, expects the strategy to load only while Memberships is active at v1's oldest version and its switch is on; an active member of a plan to be unlocked for the plan's methods, and an inactive member, another user or a guest not to be; the plan data to offer a tab and its panel only to a user with the capability; a save to store the checked methods only under the form's own nonce for that plan; and the grants step to move v1's plan meta to the plugin's key and delete it. X rulings 91 and 92. Assisted-by: Claude Code:claude-opus-5-5
v1's premium build kept a WooCommerce Memberships plan's grants in plan meta under v1's prefix and saved its plan tab without a nonce of its own. A plan's grants are its post meta under the plugin's key. WooCommerceMembershipsStrategy, gated on Memberships at v1's oldest version and on its switch, off by default, returns false on should_lock for a method a plan the user is an active member of unlocks, skipping the cancelled memberships Memberships sometimes returns. Its form shows as a tab of the plan data box through MembershipPlanTab, a host on the framework's ObjectFormHostInterface, so the form prints and verifies its own nonce for the plan, behind unlock_dws_lpm_via_wc_memberships and edit_post. The settings show the Memberships switch while Memberships is active. The fifth migration step moves v1's plan meta to the plugin's key and deletes it. Memberships is a paid plugin absent from WP Packages, so the rows run against a double of its one function and its three hooks, unverified against a licensed copy; PHPStan reads the function from a declaration in tests/PHPStan. X rulings 91 and 92. Assisted-by: Claude Code:claude-opus-5-5
v1 exposed root getters, hook-tag builders, constants, boot hooks, access checks and setting readers that returned true for "locked" under names that read the other way and took v1's field ids, so code written against v1 calls them. DeprecatedApiTest expects v1's getters and constants to answer from the plugin header, v1's hook-tag builders to build v1's names for the plugin and for v1's twelve components, v1's boot hooks to fire after the v2 ones, dws_lpmwc_is_payment_method_locked() to answer for a user, v1's access checks to fire v1's filters and then answer from the strategies, v1's per-strategy checks to answer from their strategy alone and true while it is off, and v1's setting readers to map v1's ids to the plugin's options through v1's value filters, each shim deprecated with its replacement. X rulings 94 and 95. Assisted-by: Claude Code:claude-opus-5-5
v1 exposed root getters, hook-tag builders, constants, boot hooks, access checks and setting readers that returned true for "locked" under names that read the other way, ignored the locked list and took v1's field ids, which no longer name options. includes/deprecated.php keeps them through 2.x, each deprecated with its replacement: the getters and constants answer from the plugin header, without v1's whitelabel prefix; the hook-tag builders build v1's names for the plugin and its twelve v1 components; v1's boot hooks fire after the v2 ones; the access checks fire v1's per-user and per-order filters, then answer from the strategies; the per-strategy checks answer from their strategy alone, through an internal static check each strategy's hook now wraps, and true while it is off; and the setting readers map v1's ids to the plugin's options through v1's page and group value filters. dws_lpmwc_is_payment_method_locked() answers whether a method is on the list and no strategy unlocks it for a user and an order, and the profile and Users screens ask it. X rulings 94 and 95. Assisted-by: Claude Code:claude-opus-5-5
v1 removed data only from a Freemius hook, deleting its options under an unescaped LIKE and its capabilities, and left user, order and plan grants behind. FootprintTest expects the footprint to name v1's option prefix, version rows and notices, order meta under both prefixes, the plan meta under both keys, v1's global user grants and the four capabilities. UninstallTest expects a consenting uninstall to remove the Groups map, v1's options, a user's site grants, v1's global grants, v1's notices, an order's and a plan's grants under both keys, and the capabilities. X ruling 96. Assisted-by: Claude Code:claude-opus-5-5
v1 removed data only from a Freemius hook, deleting its options under an unescaped LIKE and its capabilities, and left user, order and plan grants behind. The footprint, which the framework's uninstall engine removes where the site consents through dws_lpmwc_delete_data, adds v1's option prefix, version rows and notices; order meta under the plugin's and v1's prefixes, on either storage; the plan meta under both keys; v1's global user grants that a migration has not moved yet; and the four capabilities. The Unit bootstrap loads the files whose constants the footprint reads. X ruling 96. Assisted-by: Claude Code:claude-opus-5-5
Removing the template's demo also dropped the ignore rules for blocks/build and assets/build, so the same commit added the build output the scaffold had left in place: the example block's and the greeting's compiled scripts and styles, which nothing loads. Assisted-by: Claude Code:claude-opus-5-5
v1's readme advertised a premium build, linked a knowledge base and a contact form that no longer exist, said only enabled payment methods can be locked and placed the settings on the Payments tab; v1's version history lived only in that readme. Locked Payment Methods stays listed on wordpress.org, so the entry drops the Update URI header, which leaves the bundled updater inert, and the text-domain loader, since language packs load just in time, and states what the plugin does. readme.txt keeps v1's Description, Installation, FAQ and Screenshots, updated: the five ways of unlocking a payment method as plain features, WooCommerce 11.2 and the Advanced tab, any registered payment method lockable, grants per site on a network, support on the forum. Its Upgrade Notice names the floors, the settings' move and the explicit-false unlock, and points at the changelog. CHANGELOG.md carries v1's history between the markers that changelog:write copies into readme.txt, through the Pronamic tool now in require-dev, and 23 fragments record 2.0.0's changes. X rulings 88 and 97. Assisted-by: Claude Code:claude-opus-5-5
The Groups, Memberships and order rows hook their own strategy for the whole class and never unhooked it, so every later class in the process ran it too. In the root's shared environment, which has no Groups, the leftover Groups strategy threw on Groups_User from the user profile rows' checkout filter. Each class clones WordPress's hook table before it hooks its strategy and puts the copy back after its last row, as WordPress's own test case does around each test. Assisted-by: Claude Code:claude-opus-5-5
The template's declaration marks a new plugin incompatible with WooCommerce's order tables until its own evidence says otherwise, so WooCommerce warned on every Locked Payment Methods site that stores orders there, though the plugin reads and writes orders only through WC CRUD and its order rows run on both storages. The plugin declares itself compatible, and a boot row checks the declaration. It declares nothing for the cart and checkout blocks, whose checkout the lock covers through the gateways filter they share. X ruling 88. Assisted-by: Claude Code:claude-opus-5-5
wordpress.org reads the stable tag and the floors from readme.txt, while WordPress and the release read them from the entry's headers, and nothing kept the two in step. A Unit row compares readme.txt's Stable tag, Requires at least and Requires PHP with the entry's headers, and another checks the changelog markers the readme sync fills, as Linked Orders' does. Assisted-by: Claude Code:claude-opus-5-5
The Integration rows filter the gateways directly, so no row showed a granted customer the block checkout or sent the Store API a checkout naming a locked payment method. An end-to-end spec locks cash on delivery and opens the store, which a new WooCommerce install keeps as coming soon, then checks that a customer it is granted to sees it on the block checkout, and that the Store API refuses a guest's checkout naming it with woocommerce_rest_checkout_payment_method_disabled. Assisted-by: Claude Code:claude-opus-5-5
The plugin's README, AGENTS.md, architecture and tests docs, SECURITY scope and manifest descriptions still described the template: its demo, its fill-in, its blocks and its GitHub distribution. docs/ recipes.md, the template's guide, came with the scaffold, and phpcs.dist.xml kept the comment of the build exclusions the demo's removal dropped. Each now describes Locked Payment Methods: what it does and how other plugins lock and unlock payment methods, its strategies, its lock and v1's API, its data under dws_lpmwc_ and what the migrations move from v1, its grants forms, order storage and v1-hook conventions, and its wordpress.org distribution. Release passes wp-org: true and Quality lints readme.txt, as Linked Orders' workflows do, which the plugin's now match. They point at the template's recipes, which it maintains. Assisted-by: Claude Code:claude-opus-5-5
The root's shared environment, its Integration run and its plugins:* fan-outs named Linked Orders and Internal Comments alone, so Locked Payment Methods would have joined the CI lanes but not the shared site where the plugins are tested side by side. The shared smoke check only checked that the plugins were active, and the README's table listed Linked Orders alone. The shared environment maps and activates Locked Payment Methods, and test:integration:run runs its Integration suite there without the groups and wc-memberships groups, whose plugins the site lacks; every Composer and npm fan-out lists it. The shared checks add that every plugin boots from its own prefixed framework, and that a guest sees a locked payment method on neither the classic nor the block checkout. AGENTS.md and the README name the plugin, and the README Internal Comments too. X ruling 99. Assisted-by: Claude Code:claude-opus-5-5
Only an explicit false from dws_lpmwc/payment_method/should_lock is meant to unlock a payment method, but the strategies typed the lock as bool. WordPress calls them in weak mode, so a 0 or an empty string an earlier callback returned became false and unlocked the method for everyone, guests included, and a null threw a TypeError that broke the checkout. The lock itself took the gateways as an array, so another plugin's gateways filter returning anything else broke it too. Each strategy takes the lock untyped and returns it unchanged unless its subject unlocks the method, and the lock passes on a gateways value that is not an array, which WooCommerce casts to an empty list. Rows hook a null, a 0 and an empty string before the strategies, and a null in place of the gateways. Assisted-by: Claude Code:claude-opus-5-5
dws_lpmwc_is_payment_method_locked() and the grants forms read the settings' locked list, while checkout also locks the ids the list filters add, and their docblocks called it "the locked list", which reads as the list checkout uses. The docblocks and the architecture name the settings' list and say why the list filters stay out: they judge the payment methods a checkout offers, as v1's did, so a callback written for a cart never runs on a profile or the Users screen. Assisted-by: Claude Code:claude-opus-5-5
The Users screen's filter narrowed every user query run on users.php through pre_get_users, as v1's did, so another plugin's user query on that screen, such as a dropdown or a count, saw only the users granted the filtered payment method. The filter adds its meta query on users_list_table_query_args, which only the list tables apply to their own query, and a row checks that a query of its own on the Users screen keeps every user. Assisted-by: Claude Code:claude-opus-5-5
WordPress uninstalls a plugin in a request that never included it, and uninstall.php loaded footprint.php and options.php but not the capabilities and grants files whose constants the footprint names. The footprint threw on an undefined constant, so a web uninstall kept every row despite the site's consent, and WP-CLI failed. The uninstall rows ran in processes that had loaded the plugin, which defined them. uninstall.php requires capabilities.php and grants.php too, and a row uninstalls through a WP-CLI request that skips the plugin, as WordPress's own uninstall request does. Assisted-by: Claude Code:claude-opus-5-5
The order grants step queued the deletion of v1's grant rows and the new list in one save, and WooCommerce deletes an order's meta before it adds any. When the list's insert failed, the step threw after v1's rows were gone, so its retry found nothing to move and the order lost its grants. The step saves the merged list and verifies it, then deletes v1's rows in a second save and verifies that, so a failure at either save leaves what a retry needs. A row refuses the list's insert on the posts storage, then checks that v1's grant survives and the retry moves it. Assisted-by: Claude Code:claude-opus-5-5
The block checkout and the Store API's checkout leave a draft order each time the checkout spec runs, and an uninstall row created an order before checking a capability outside its try, so a failed check left the order behind. The tests site's start enables WooCommerce's order tables, whose check refuses while any order is unsynced, so the next quality-check failed to start the site. The checkout spec deletes every draft order after its rows, and the uninstall row checks the capability inside the try whose finally deletes the order. Assisted-by: Claude Code:claude-opus-5-5
The lock passed should_lock the order an order-pay URL names once the request carried that order's key. WordPress parses the order-pay query var on AJAX and REST requests too, and WooCommerce's AJAX checkout counts as the checkout, so a customer posting the cart checkout to ?wc-ajax=checkout&order-pay=<id>&key=<key> of an order they hold had that order's unlocked payment methods offered for a new order from the cart. The Store API's checkout took the same path. The order-pay order counts only outside AJAX and REST requests, where the order-pay page and its pay form run, and rows give an AJAX and a REST request a valid key. The changelog's security entry says so. Assisted-by: Claude Code:claude-opus-5-5
The boot row asserted that includes/_disabled-example.php never loads, but no such file exists, so the row passed even with the loader's underscore check removed. The row writes an underscore-prefixed include defining a constant and checks in a fresh request that the constant stays undefined, as Linked Orders' row does; it fails once the loader's check is removed. Assisted-by: Claude Code:claude-opus-5-5
The order grants step probed for each meta table with SHOW TABLES and skipped a table the probe did not name, so a failed probe read as an absent table. The step then finished, the runner recorded it, and the orders in that table kept v1's grants, which v2 never reads. A probe that fails throws, so the runner runs the step again on a later request, and a row breaks the probe and expects the throw. Assisted-by: Claude Code:claude-opus-5-5
The footprint swept every user meta key starting with dws_wc_lpm_,
though v1 stored only its grants, dws_wc_lpm_grant_access_{gateway},
under it. A user meta prefix sweep also matches WordPress's own
per-site keys, such as {table prefix}capabilities, when the site's
table prefix starts with it, so the wider prefix put every user's
roles at risk on a site whose table prefix begins dws_wc_lpm_.
The footprint names v1's grants prefix alone, and the Unit row and the
architecture say so.
Assisted-by: Claude Code:claude-opus-5-5
The lock passed on a gateways value that was not an array, so an earlier callback that returned the gateways as an object skipped the lock, and WooCommerce, which casts the filtered value to an array, then offered every locked payment method. The lock casts the value as WooCommerce does before it judges it, and rows replace the gateways with null and with an object. Assisted-by: Claude Code:claude-opus-5-5
dws_lpmwc/payment_method/locked_ids fired only in the checkout's gateways filter, so a payment method it locked was hidden at checkout but reported unlocked by dws_lpmwc_is_payment_method_locked() and offered on no grants form, leaving it to full-access roles and should_lock callbacks. The rows check that the lock check and the profile form count an id the filter adds, and that the filter runs right after v1's value filter with the ids alone, before v1's list filter at checkout. Assisted-by: Claude Code:claude-opus-5-5
dws_lpmwc/payment_method/locked_ids fired only in the checkout's gateways filter with the offered gateways, so the lock check, the grants forms and the Users filter read a shorter list than checkout did, and a payment method locked from code could be unlocked only by full-access roles or should_lock callbacks. The filter fires inside dws_lpmwc_get_locked_payment_method_ids() with the ids alone, after v1's value filter, so every reader sees the ids it adds; v1's deprecated list filter still fires only at checkout, with the offered gateways, as v1 fired it. Assisted-by: Claude Code:claude-opus-5-5
The Users column and the grants choices asked dws_lpmwc_is_payment_method_locked() about ids they had just read from the locked list, so every row and payment method read the options and ran the list filters again, locked_ids callbacks included. Both call dws_lpmwc_should_lock() for the id they already know is locked, which judges it the same way. Assisted-by: Claude Code:claude-opus-5-5
The row read "skipped" from a fresh request whenever the constant the underscore include defines stayed undefined, which also happens when the plugin never loads or boots in that request or the include was never written, so the row could pass without testing the loader. The row checks the write and the request's exit status, and prints "not booted" unless the plugin's boot succeeded. Assisted-by: Claude Code:claude-opus-5-5
ahegyes
force-pushed
the
feat/lpm-port
branch
from
November 1, 2026 10:51
c46313c to
aa509c6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ports Locked Payment Methods for WooCommerce 1.3.5 to the v2 framework as 2.0.0, with every way of unlocking a payment method it had, distributed through wordpress.org, adds it to the monorepo's fan-outs and shared test environment, and moves Linked Orders' and Internal Comments' safeguards before the floor gate, as the template loads them.
Type of change
Checklist
composer changelog:addin the changed plugin's directory, or at the root for the monorepo itself)composer quality-checkpasses locally in every changed pluginreadme.txtupdated if runtime requirements changedWhat changes
includes/safeguards.phpbefore the bootstrap package and its floor gate, so a restriction either adds holds below its floors too.falsefromdws_lpmwc/payment_method/should_lock, once the plugin has booted, unlocks a payment method, anddws_lpmwc/payment_method/locked_ids, which the lock check and the grants forms read too, only adds ids.dws_lpmwc_options; any registered payment method can be locked.Breaking changes
falseunlocks a payment method; a list filter only adds ids; an empty full-access roles list unlocks no one.init_statusfilters, its container and lifecycle functions and Freemius are removed.Not verified locally
wc-locked-payment-methodsmirror needs awp-org-releaseenvironment withSVN_USERNAMEandSVN_PASSWORDbefore the first wordpress.org release.