wc-coupon-dynamic
Build or audit WooCommerce virtual coupons resolved at runtime without a `shop_coupon` row. Covers `woocommerce_get_shop_coupon_data`, the `read_manual_coupon()` data contract, reserved code namespaces and resolver precedence, database-fallback collisions, request caching, Store
Install
npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/woocommerce/wc-coupon-dynamic
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
git clone https://github.com/Lonsdale201/wp-agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
WooCommerce virtual coupons
WooCommerce calls these virtual coupons. “Pseudo coupon” is an informal description, not the core term. Use one when a code is generated or resolved from another authoritative store and creating a shop_coupon post per code would cause unnecessary synchronization.
Choose the right model
| Need | Model |
|---|---|
| Merchant edits the code; core reporting, holds, and usage limits should work | Persisted WC_Coupon; use wc-coupon-types-rules |
| A namespaced code maps to an external entitlement | Virtual coupon from this skill |
| A new discount formula appears in the coupon type selector | Register a complete custom type with wc-coupon-types-rules; it may also be used by a virtual coupon |
| A surcharge or positive adjustment | WooCommerce fee API, not a negative coupon |
A virtual coupon is not automatically safer or faster. Its resolver and usage ledger replace storage and concurrency behavior that core normally supplies.
Understand the resolution contract
WC_Coupon::__construct() filters the unresolved value before database lookup:
$coupon = apply_filters( 'woocommerce_get_shop_coupon_data', false, $data, $this );
A truthy result is passed to read_manual_coupon(), which sets ID 0, marks the object virtual, and skips persisted lookup. Returning false means “not resolved by this filter” and lets later filters or the database handle the input.
The input may be an integer ID or a string code. Do not type it as string. The filter can run on frontend, Store API, REST-adjacent order operations, admin, CLI, cron, and repeated calculation paths.
Use an owned namespace and one request snapshot
final class MyPlugin_Virtual_Coupons {
private const PREFIX = 'loyalty-';
/** @var array<string,object|null> */
private static $entitlements = array();
public static function normalize( $input ): ?string {
if ( ! is_string( $input ) ) {
return null;
}
$code = wc_strtolower( wc_format_coupon_code( $input ) );
return 0 === strpos( $code, self::PREFIX ) ? $code : null;
}
public static function entitlement( string $code ) {
if ( ! array_key_exists( $code, self::$entitlements ) ) {
self::$entitlements[ $code ] = myplugin_find_entitlement( $code );
}
return self::$entitlements[ $code ];
}
public static function resolve( $resolved, $input ) {
if ( false !== $resolved ) {
return $resolved; // Preserve a resolver that ran earlier.
}
$code = self::normalize( $input );
if ( null === $code ) {
return false;
}
$entitlement = self::entitlement( $code );
// Keep every owned-prefix code virtual, including denied/unknown ones.
// Validation below rejects this inert marker without database fallback.
if ( ! $entitlement ) {
return array(
'discount_type' => 'fixed_cart',
'amount' => '0',
'description' => __( 'Unavailable virtual coupon', 'myplugin' ),
);
}
return array(
'discount_type' => 'percent',
'amount' => '10',
'individual_use' => true,
'usage_limit' => 1,
'usage_count' => (int) $entitlement->usage_count,
'date_expires' => $entitlement->expires_at,
'exclude_sale_items' => true,
'description' => __( 'Loyalty discount', 'myplugin' ),
);
}
}
add_filter(
'woocommerce_get_shop_coupon_data',
array( MyPlugin_Virtual_Coupons::class, 'resolve' ),
10,
2
);
Claim only a cheap, namespaced prefix before database or HTTP work. A request cache is not merely an optimization: it keeps repeated validation/calculation within one request on the same entitlement snapshot.
Prevent fallback collisions
If an invalid owned-prefix code returns false, Woo may load a persisted coupon with the same normalized code. Choose and enforce one of these policies:
- prohibit persisted coupons in the reserved namespace; or
- return an inert virtual marker for every owned-prefix code and reject unknown/unauthorized markers in validation, as above.
Do not return a malformed array. The marker must be a valid, harmless coupon object and must fail closed in the validation layer.
Validate without side effects
add_filter(
'woocommerce_coupon_is_valid',
static function ( bool $valid, WC_Coupon $coupon, WC_Discounts $discounts ): bool {
$code = MyPlugin_Virtual_Coupons::normalize( $coupon->get_code() );
if ( null === $code ) {
return $valid;
}
$entitlement = MyPlugin_Virtual_Coupons::entitlement( $code );
$context = $discounts->get_object();
$user_id = $context instanceof WC_Order
? (int) $context->get_customer_id()
: get_current_user_id();
if ( ! $entitlement || ! $user_id ) {
return false;
}
return $valid
&& (int) $entitlement->user_id === $user_id
&& myplugin_user_can_redeem( $user_id, $code );
},
10,
3
);
Use woocommerce_coupon_is_valid_for_product for per-line eligibility and woocommerce_coupon_is_valid for coupon-wide rules. Validation may run repeatedly against a WC_Cart or WC_Order; keep it deterministic, bounded, and side-effect free. Never consume entitlement during validation or calculation.
For orders, validate the order customer's entitlement, not the administrator or CLI actor. Authorize the caller separately before permitting an order mutation. This user-bound example deliberately rejects guests. recalculate_coupons() skips coupon-wide validation; historical replay needs a saved rule policy, not this callback.
Returning false gives core's filtered-invalid error. A callback may deliberately throw an Exception for a customer-safe custom denial message because WC_Discounts catches it, but do not leak whether another user's entitlement exists.
Supply canonical manual data
read_manual_coupon() accepts the same property names used by WC_Coupon::set_props():
| Key | Expected value |
|---|---|
discount_type, amount |
registered type; decimal-compatible amount |
individual_use, exclude_sale_items, free_shipping |
booleans |
product_ids, excluded_product_ids |
arrays of product IDs |
product_categories, excluded_product_categories |
arrays of product_cat term IDs |
minimum_amount, maximum_amount |
decimal-compatible values |
usage_limit, usage_limit_per_user, limit_usage_to_x_items, usage_count |
integers |
date_expires |
parseable date, timestamp, or WC_DateTime |
email_restrictions |
array of billing email patterns |
description |
string |
Use the canonical date_expires; expiry_date is only a compatibility alias. Use booleans rather than 'yes'/'no', and integer arrays rather than comma-separated IDs. Do not call save() on an ID-zero virtual coupon; that changes the storage model.
Own atomic usage accounting
Virtual does not disable all native validation:
- global
usage_limitis compared with the suppliedusage_count; - a filter-resolved virtual object has no coupon data store, so checkout skips native tentative holds and core cannot increment/decrement it;
- native per-user history and tentative checkout holds require a persisted coupon ID, so
usage_limit_per_useris not sufficient for a virtual coupon; - there is no core concurrency reservation for an external entitlement.
Use an owned ledger/table with a unique key such as (order_id, normalized_coupon_code). Atomically reserve or consume a slot at one documented lifecycle boundary, record repeated callbacks idempotently, and define cancellation, failed-payment, expiry, and refund reversal policy. Do not implement a counter as get_option() followed by update_option( $count + 1 ).
If strict single-use protection is required before payment, create an expiring reservation tied to the checkout/order and finalize it after the chosen success event. Release abandoned reservations. Treat resolver usage_count as display/validation input, not as the concurrency lock.
Preserve order snapshots
Normal checkout creates a coupon order item and writes Woo's compact coupon_info snapshot. It contains ID, code, type, nominal amount, and optional free-shipping flag. Do not extend that JSON array; store custom immutable facts as separate namespaced coupon-line metadata through woocommerce_checkout_create_order_coupon_item.
Historical recalculation may reconstruct an ID-zero/missing coupon from coupon_info. Your custom discount-type registration and calculation must still be loaded. If the result depends on mutable external state, snapshot the required rate/tier/rule outcome and restore it through woocommerce_order_recalculate_coupons_coupon_object rather than calling today's entitlement service.
Direct application to an existing order
WC_Order::apply_coupon() recalculates item and tax totals, but the direct virtual-object path has a snapshot trap: core later performs an ID lookup and may construct new WC_Coupon( 0 ), losing the original virtual type/amount before it stores coupon_info.
apply_coupon() performs its first recalculation before returning. Repairing the snapshot afterward is too late: a restricted percentage coupon can already have been replayed as an unrestricted fixed-cart amount. Install an order-and-code-scoped restoration filter before applying, remove it in finally, and preserve a separate property/rule snapshot for later recalculations. Read references/order-application.md for the pattern.
The compact core snapshot omits product/category restrictions, sale exclusions and quantity limits. Test per-line allocation immediately after application and again after reload with the resolver unavailable. A correct grand total alone can hide discounts allocated to excluded products.
Fee and shipping rows are not product discount targets. Reject an inapplicable monetary entitlement before reserving it; a successful order API return alone does not prove value was delivered. See the fee-only guard in wc-coupon-types-rules.
Support every shopper surface
Cart and Checkout Blocks/Store API still construct server-side WC_Coupon and use WC_Discounts, so a globally loaded resolver and validation filters work there. Do not limit hooks to classic form requests. Test:
- classic cart and checkout;
- Cart and Checkout Blocks / Store API apply and remove;
- guest and authenticated identity changes;
- repeated totals calculation and checkout retries;
- admin order application and recalculation;
- concurrent last-slot redemption;
- cancellation, failed payment, full/partial refund policy;
- code collision with a persisted coupon;
- custom type plugin deactivation and historical recalculation.
Prefer simple normalized code characters. The Store API's coupon endpoints and older/by-code route patterns do not all accept identical arbitrary characters.
Cross-references
wc-coupon-types-rules: persisted coupon CRUD, complete custom discount types, native/custom rules, stacking, holds, and the full regression matrix.wc-order-lifecycle-and-items: safe idempotent order status and refund side effects.wc-cart-checkout-classic: classic cart calculation and checkout transfer.
References
- Direct virtual order application and historical replay
- Disposable CLI smoke example
- Verified source paths:
wp-content/plugins/woocommerce/includes/class-wc-coupon.phpwp-content/plugins/woocommerce/includes/class-wc-discounts.phpwp-content/plugins/woocommerce/includes/abstracts/abstract-wc-order.phpwp-content/plugins/woocommerce/includes/class-wc-order.phpwp-content/plugins/woocommerce/includes/wc-coupon-functions.phpwp-content/plugins/woocommerce/src/StoreApi/Utilities/CartController.php
Files (wp-agent-skills)
-
references
-
order-application.md 4.8 KB
# Virtual coupons on existing orders Source and runtime scope: WooCommerce 11.1.1. Use this only for a virtual coupon applied by trusted code to an existing order. Authorize the caller and validate the order customer's entitlement before mutation. The caller is not necessarily the customer. ## Protect the first recalculation `WC_Abstract_Order::apply_coupon()` creates coupon items, saves, calls `recalculate_coupons()`, then updates usage. Its coupon-item creation looks up the code's persisted ID. For an ID-zero virtual coupon it can store an empty fixed-cart snapshot, losing the original percentage/type and restrictions. A patch after the call cannot protect that first replay. Wrap the call with a temporary filter scoped to the exact order object and code. This example snapshots property-driven rules; snapshot custom policy inputs separately if calculation hooks require them. ```php $policy = array( 'discount_type' => $virtual_coupon->get_discount_type(), 'amount' => $virtual_coupon->get_amount(), 'product_ids' => $virtual_coupon->get_product_ids(), 'excluded_product_ids' => $virtual_coupon->get_excluded_product_ids(), 'product_categories' => $virtual_coupon->get_product_categories(), 'excluded_product_categories' => $virtual_coupon->get_excluded_product_categories(), 'exclude_sale_items' => $virtual_coupon->get_exclude_sale_items(), 'limit_usage_to_x_items' => $virtual_coupon->get_limit_usage_to_x_items(), ); $restore = static function ( $candidate, $code, $item, $context ) use ( $order, $virtual_coupon, $policy ) { if ( $context !== $order || ! wc_is_same_coupon( $code, $virtual_coupon->get_code() ) ) { return $candidate; } $item->update_meta_data( 'coupon_info', $virtual_coupon->get_short_info() ); $item->update_meta_data( '_myplugin_coupon_policy_v1', $policy ); $item->save(); return $virtual_coupon; }; add_filter( 'woocommerce_order_recalculate_coupons_coupon_object', $restore, 10, 4 ); try { $result = $order->apply_coupon( $virtual_coupon ); } finally { remove_filter( 'woocommerce_order_recalculate_coupons_coupon_object', $restore, 10 ); } if ( is_wp_error( $result ) ) { // Return a customer-safe error; do not consume an external entitlement. return $result; } ``` The core `coupon_info` value above is the original, unmodified `get_short_info()` result. Keep extensions in separate namespaced metadata. Reserve an external entitlement atomically according to your checkout/order policy; compensate on failure. `finally` removes the filter but is not a database transaction or business rollback. ## Restore saved policy on later requests The temporary filter is gone on subsequent requests. Register a permanent restoration filter that recognizes only your owned code namespace and versioned, server-written metadata. Validate its shape and allowlist property names before passing data to `set_props()`; do not trust arbitrary request/REST metadata. Use the saved policy rather than re-querying current customer tiers or coupon definitions. In that callback: 1. Preserve the incoming object for unrelated coupon lines. 2. Read `_myplugin_coupon_policy_v1`; fail the operation explicitly if an owned historical policy is missing or invalid and deterministic replay is required. 3. Restore the allowlisted properties and check the `set_props()` result for `WP_Error`. 4. Return the restored coupon. Keep custom type/calculation callbacks loaded on admin, CLI and background requests too. Core replay calls the discount engine with coupon-wide validation disabled. It does not enforce a new redemption's expiry, membership or usage-limit checks. The property snapshot still governs line selection; any external per-product/calculation filter needs a historical policy too. This mechanism does not freeze tax rates, product taxability or every other integration's behavior. ## Regression fixture Use two untaxed products costing 100 each and a virtual 10% coupon restricted to the first product. Assert **90 / 100** line totals and a **190** order total immediately after `apply_coupon()`, then after reloading and recalculating with the resolver absent. Checking only 190 can miss an incorrect **95 / 95** allocation. Also test actor A / order customer B / entitlement B, CLI actor 0, a denied entitlement, a missing snapshot, and fee-only orders. The [smoke plugin example](../../wcs-upgrade-compatibility/examples/woo-skills-smoke/woo-skills-smoke.php) demonstrates positive restoration and customer-identity assertions. ## Sources - `woocommerce/includes/abstracts/abstract-wc-order.php`: `apply_coupon()`, `set_coupon_discount_amounts()`, `recalculate_coupons()`. - `woocommerce/includes/class-wc-coupon.php`: `get_short_info()`, `set_props()` via `WC_Data`. - [WooCommerce 11.1.1 source](https://github.com/woocommerce/woocommerce/tree/11.1.1/plugins/woocommerce).
-
-
SKILL.md 12.5 KB
--- name: wc-coupon-dynamic description: Build or audit WooCommerce virtual coupons resolved at runtime without a `shop_coupon` row. Covers `woocommerce_get_shop_coupon_data`, the `read_manual_coupon()` data contract, reserved code namespaces and resolver precedence, database-fallback collisions, request caching, Store API and classic checkout behavior, validation, external atomic usage accounting, order coupon snapshots, direct order application, deterministic recalculation, and security. Use for generated loyalty, referral, partner, campaign, or entitlement codes backed by an owned table/service, or when code calls these APIs. For persisted coupons, new discount-type math, or general coupon rules use `wc-coupon-types-rules`. metadata: wp-skills-author: "Soczó Kristóf" wp-skills-contact: "mailto:lonsdale201@hotmail.com" wp-skills-plugin: "woocommerce" wp-skills-plugin-version-tested: "11.1.1" wp-skills-php-min: "7.4" wp-skills-last-updated: "2026-09-21" --- # WooCommerce virtual coupons WooCommerce calls these **virtual coupons**. “Pseudo coupon” is an informal description, not the core term. Use one when a code is generated or resolved from another authoritative store and creating a `shop_coupon` post per code would cause unnecessary synchronization. ## Choose the right model | Need | Model | |---|---| | Merchant edits the code; core reporting, holds, and usage limits should work | Persisted `WC_Coupon`; use `wc-coupon-types-rules` | | A namespaced code maps to an external entitlement | Virtual coupon from this skill | | A new discount formula appears in the coupon type selector | Register a complete custom type with `wc-coupon-types-rules`; it may also be used by a virtual coupon | | A surcharge or positive adjustment | WooCommerce fee API, not a negative coupon | A virtual coupon is not automatically safer or faster. Its resolver and usage ledger replace storage and concurrency behavior that core normally supplies. ## Understand the resolution contract `WC_Coupon::__construct()` filters the unresolved value before database lookup: ```php $coupon = apply_filters( 'woocommerce_get_shop_coupon_data', false, $data, $this ); ``` A truthy result is passed to `read_manual_coupon()`, which sets ID `0`, marks the object virtual, and skips persisted lookup. Returning `false` means “not resolved by this filter” and lets later filters or the database handle the input. The input may be an integer ID or a string code. Do not type it as `string`. The filter can run on frontend, Store API, REST-adjacent order operations, admin, CLI, cron, and repeated calculation paths. ## Use an owned namespace and one request snapshot ```php final class MyPlugin_Virtual_Coupons { private const PREFIX = 'loyalty-'; /** @var array<string,object|null> */ private static $entitlements = array(); public static function normalize( $input ): ?string { if ( ! is_string( $input ) ) { return null; } $code = wc_strtolower( wc_format_coupon_code( $input ) ); return 0 === strpos( $code, self::PREFIX ) ? $code : null; } public static function entitlement( string $code ) { if ( ! array_key_exists( $code, self::$entitlements ) ) { self::$entitlements[ $code ] = myplugin_find_entitlement( $code ); } return self::$entitlements[ $code ]; } public static function resolve( $resolved, $input ) { if ( false !== $resolved ) { return $resolved; // Preserve a resolver that ran earlier. } $code = self::normalize( $input ); if ( null === $code ) { return false; } $entitlement = self::entitlement( $code ); // Keep every owned-prefix code virtual, including denied/unknown ones. // Validation below rejects this inert marker without database fallback. if ( ! $entitlement ) { return array( 'discount_type' => 'fixed_cart', 'amount' => '0', 'description' => __( 'Unavailable virtual coupon', 'myplugin' ), ); } return array( 'discount_type' => 'percent', 'amount' => '10', 'individual_use' => true, 'usage_limit' => 1, 'usage_count' => (int) $entitlement->usage_count, 'date_expires' => $entitlement->expires_at, 'exclude_sale_items' => true, 'description' => __( 'Loyalty discount', 'myplugin' ), ); } } add_filter( 'woocommerce_get_shop_coupon_data', array( MyPlugin_Virtual_Coupons::class, 'resolve' ), 10, 2 ); ``` Claim only a cheap, namespaced prefix before database or HTTP work. A request cache is not merely an optimization: it keeps repeated validation/calculation within one request on the same entitlement snapshot. ### Prevent fallback collisions If an invalid owned-prefix code returns `false`, Woo may load a persisted coupon with the same normalized code. Choose and enforce one of these policies: 1. prohibit persisted coupons in the reserved namespace; or 2. return an inert virtual marker for every owned-prefix code and reject unknown/unauthorized markers in validation, as above. Do not return a malformed array. The marker must be a valid, harmless coupon object and must fail closed in the validation layer. ## Validate without side effects ```php add_filter( 'woocommerce_coupon_is_valid', static function ( bool $valid, WC_Coupon $coupon, WC_Discounts $discounts ): bool { $code = MyPlugin_Virtual_Coupons::normalize( $coupon->get_code() ); if ( null === $code ) { return $valid; } $entitlement = MyPlugin_Virtual_Coupons::entitlement( $code ); $context = $discounts->get_object(); $user_id = $context instanceof WC_Order ? (int) $context->get_customer_id() : get_current_user_id(); if ( ! $entitlement || ! $user_id ) { return false; } return $valid && (int) $entitlement->user_id === $user_id && myplugin_user_can_redeem( $user_id, $code ); }, 10, 3 ); ``` Use `woocommerce_coupon_is_valid_for_product` for per-line eligibility and `woocommerce_coupon_is_valid` for coupon-wide rules. Validation may run repeatedly against a `WC_Cart` or `WC_Order`; keep it deterministic, bounded, and side-effect free. Never consume entitlement during validation or calculation. For orders, validate the order customer's entitlement, not the administrator or CLI actor. Authorize the caller separately before permitting an order mutation. This user-bound example deliberately rejects guests. `recalculate_coupons()` skips coupon-wide validation; historical replay needs a saved rule policy, not this callback. Returning `false` gives core's filtered-invalid error. A callback may deliberately throw an `Exception` for a customer-safe custom denial message because `WC_Discounts` catches it, but do not leak whether another user's entitlement exists. ## Supply canonical manual data `read_manual_coupon()` accepts the same property names used by `WC_Coupon::set_props()`: | Key | Expected value | |---|---| | `discount_type`, `amount` | registered type; decimal-compatible amount | | `individual_use`, `exclude_sale_items`, `free_shipping` | booleans | | `product_ids`, `excluded_product_ids` | arrays of product IDs | | `product_categories`, `excluded_product_categories` | arrays of `product_cat` term IDs | | `minimum_amount`, `maximum_amount` | decimal-compatible values | | `usage_limit`, `usage_limit_per_user`, `limit_usage_to_x_items`, `usage_count` | integers | | `date_expires` | parseable date, timestamp, or `WC_DateTime` | | `email_restrictions` | array of billing email patterns | | `description` | string | Use the canonical `date_expires`; `expiry_date` is only a compatibility alias. Use booleans rather than `'yes'`/`'no'`, and integer arrays rather than comma-separated IDs. Do not call `save()` on an ID-zero virtual coupon; that changes the storage model. ## Own atomic usage accounting Virtual does not disable all native validation: - global `usage_limit` is compared with the supplied `usage_count`; - a filter-resolved virtual object has no coupon data store, so checkout skips native tentative holds and core cannot increment/decrement it; - native per-user history and tentative checkout holds require a persisted coupon ID, so `usage_limit_per_user` is not sufficient for a virtual coupon; - there is no core concurrency reservation for an external entitlement. Use an owned ledger/table with a unique key such as `(order_id, normalized_coupon_code)`. Atomically reserve or consume a slot at one documented lifecycle boundary, record repeated callbacks idempotently, and define cancellation, failed-payment, expiry, and refund reversal policy. Do not implement a counter as `get_option()` followed by `update_option( $count + 1 )`. If strict single-use protection is required before payment, create an expiring reservation tied to the checkout/order and finalize it after the chosen success event. Release abandoned reservations. Treat resolver `usage_count` as display/validation input, not as the concurrency lock. ## Preserve order snapshots Normal checkout creates a coupon order item and writes Woo's compact `coupon_info` snapshot. It contains ID, code, type, nominal amount, and optional free-shipping flag. Do not extend that JSON array; store custom immutable facts as separate namespaced coupon-line metadata through `woocommerce_checkout_create_order_coupon_item`. Historical recalculation may reconstruct an ID-zero/missing coupon from `coupon_info`. Your custom discount-type registration and calculation must still be loaded. If the result depends on mutable external state, snapshot the required rate/tier/rule outcome and restore it through `woocommerce_order_recalculate_coupons_coupon_object` rather than calling today's entitlement service. ### Direct application to an existing order `WC_Order::apply_coupon()` recalculates item and tax totals, but the direct virtual-object path has a snapshot trap: core later performs an ID lookup and may construct `new WC_Coupon( 0 )`, losing the original virtual type/amount before it stores `coupon_info`. `apply_coupon()` performs its first recalculation **before returning**. Repairing the snapshot afterward is too late: a restricted percentage coupon can already have been replayed as an unrestricted fixed-cart amount. Install an order-and-code-scoped restoration filter before applying, remove it in `finally`, and preserve a separate property/rule snapshot for later recalculations. Read [references/order-application.md](references/order-application.md) for the pattern. The compact core snapshot omits product/category restrictions, sale exclusions and quantity limits. Test per-line allocation immediately after application and again after reload with the resolver unavailable. A correct grand total alone can hide discounts allocated to excluded products. Fee and shipping rows are not product discount targets. Reject an inapplicable monetary entitlement before reserving it; a successful order API return alone does not prove value was delivered. See the fee-only guard in `wc-coupon-types-rules`. ## Support every shopper surface Cart and Checkout Blocks/Store API still construct server-side `WC_Coupon` and use `WC_Discounts`, so a globally loaded resolver and validation filters work there. Do not limit hooks to classic form requests. Test: - classic cart and checkout; - Cart and Checkout Blocks / Store API apply and remove; - guest and authenticated identity changes; - repeated totals calculation and checkout retries; - admin order application and recalculation; - concurrent last-slot redemption; - cancellation, failed payment, full/partial refund policy; - code collision with a persisted coupon; - custom type plugin deactivation and historical recalculation. Prefer simple normalized code characters. The Store API's coupon endpoints and older/by-code route patterns do not all accept identical arbitrary characters. ## Cross-references - `wc-coupon-types-rules`: persisted coupon CRUD, complete custom discount types, native/custom rules, stacking, holds, and the full regression matrix. - `wc-order-lifecycle-and-items`: safe idempotent order status and refund side effects. - `wc-cart-checkout-classic`: classic cart calculation and checkout transfer. ## References - [Direct virtual order application and historical replay](references/order-application.md) - [Disposable CLI smoke example](../wcs-upgrade-compatibility/examples/woo-skills-smoke/woo-skills-smoke.php) - Verified source paths: - `wp-content/plugins/woocommerce/includes/class-wc-coupon.php` - `wp-content/plugins/woocommerce/includes/class-wc-discounts.php` - `wp-content/plugins/woocommerce/includes/abstracts/abstract-wc-order.php` - `wp-content/plugins/woocommerce/includes/class-wc-order.php` - `wp-content/plugins/woocommerce/includes/wc-coupon-functions.php` - `wp-content/plugins/woocommerce/src/StoreApi/Utilities/CartController.php`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.