Commit dad98f94cff for woocommerce

commit dad98f94cff17a89315971aaf281e622c5d99ec6
Author: daledupreez <dale@automattic.com>
Date:   Tue Jun 30 17:14:08 2026 +0200

    Require explicit class constant visibility for woocommerce-subscriptions-engine (#66066)

    - Add the PSR12 ConstantVisibility sniff as a PHPCS error
    - Set explicit visibility on all engine class constants
    - Make version and log-source constants non-public with public accessors

diff --git a/packages/php/woocommerce-subscriptions-engine/changelog/add-phpcs-sniff-to-require-const-visibility b/packages/php/woocommerce-subscriptions-engine/changelog/add-phpcs-sniff-to-require-const-visibility
new file mode 100644
index 00000000000..bdf197f0d58
--- /dev/null
+++ b/packages/php/woocommerce-subscriptions-engine/changelog/add-phpcs-sniff-to-require-const-visibility
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Require explicit visibility for class constants
diff --git a/packages/php/woocommerce-subscriptions-engine/phpcs.xml b/packages/php/woocommerce-subscriptions-engine/phpcs.xml
index 0c10b25c26d..22a3255832d 100644
--- a/packages/php/woocommerce-subscriptions-engine/phpcs.xml
+++ b/packages/php/woocommerce-subscriptions-engine/phpcs.xml
@@ -6,6 +6,12 @@
 	<!-- Define files and folders to scan -->
 	<file>.</file>

+	<!-- Require class constants to have explicit visibility -->
+	<rule ref="PSR12.Properties.ConstantVisibility.NotFound">
+		<type>error</type>
+		<exclude-pattern>tests/</exclude-pattern>
+	</rule>
+
 	<!-- Exclude test files from FileName rules -->
 	<rule ref="WordPress.Files.FileName">
 		<exclude-pattern>tests/*</exclude-pattern>
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/Contract.php b/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/Contract.php
index 67c5dbe9d84..8e47e0ae920 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/Contract.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/Contract.php
@@ -39,11 +39,11 @@ defined( 'ABSPATH' ) || exit;
  */
 final class Contract {

-	const SCHEDULE_SOURCE_PRIMITIVE = 'primitive';
-	const SCHEDULE_SOURCE_GATEWAY   = 'gateway';
+	public const SCHEDULE_SOURCE_PRIMITIVE = 'primitive';
+	public const SCHEDULE_SOURCE_GATEWAY   = 'gateway';

-	const ADDRESS_BILLING  = 'billing';
-	const ADDRESS_SHIPPING = 'shipping';
+	public const ADDRESS_BILLING  = 'billing';
+	public const ADDRESS_SHIPPING = 'shipping';

 	/**
 	 * Contract id, or null before it is persisted.
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/ContractStatus.php b/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/ContractStatus.php
index bf9fdcb55d6..d4e10fd4db7 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/ContractStatus.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/ContractStatus.php
@@ -21,11 +21,11 @@ defined( 'ABSPATH' ) || exit;
  */
 final class ContractStatus {

-	const ACTIVE               = 'active';
-	const ON_HOLD              = 'on-hold';
-	const PENDING_CANCELLATION = 'pending-cancellation';
-	const CANCELLED            = 'cancelled';
-	const EXPIRED              = 'expired';
+	public const ACTIVE               = 'active';
+	public const ON_HOLD              = 'on-hold';
+	public const PENDING_CANCELLATION = 'pending-cancellation';
+	public const CANCELLED            = 'cancelled';
+	public const EXPIRED              = 'expired';

 	/**
 	 * All known statuses.
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/Cycle.php b/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/Cycle.php
index 420aee73873..be41fc42d19 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/Cycle.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/Cycle.php
@@ -32,7 +32,7 @@ defined( 'ABSPATH' ) || exit;
  */
 final class Cycle {

-	const KIND_BILLING = 'billing';
+	public const KIND_BILLING = 'billing';

 	/**
 	 * Cycle id, or null before it is persisted.
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/CycleStatus.php b/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/CycleStatus.php
index 672d7f7ff9e..59f081d03db 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/CycleStatus.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/CycleStatus.php
@@ -28,10 +28,10 @@ defined( 'ABSPATH' ) || exit;
  */
 final class CycleStatus {

-	const PENDING   = 'pending';
-	const BILLED    = 'billed';
-	const FAILED    = 'failed';
-	const CANCELLED = 'cancelled';
+	public const PENDING   = 'pending';
+	public const BILLED    = 'billed';
+	public const FAILED    = 'failed';
+	public const CANCELLED = 'cancelled';

 	/**
 	 * The status string this value wraps.
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/Plan.php b/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/Plan.php
index 92d6a3e2acf..43d02403c09 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/Plan.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/Plan.php
@@ -26,17 +26,17 @@ defined( 'ABSPATH' ) || exit;
  */
 final class Plan {

-	const DEFAULT_CATEGORY = 'SUBSCRIPTION';
+	public const DEFAULT_CATEGORY = 'SUBSCRIPTION';

-	const DEFAULT_STATUS = 'active';
+	public const DEFAULT_STATUS = 'active';

-	const STATUS_ACTIVE = 'active';
+	public const STATUS_ACTIVE = 'active';

-	const STATUS_ARCHIVED = 'archived';
+	public const STATUS_ARCHIVED = 'archived';

-	const ALLOWED_STATUSES = array( self::STATUS_ACTIVE, self::STATUS_ARCHIVED );
+	public const ALLOWED_STATUSES = array( self::STATUS_ACTIVE, self::STATUS_ARCHIVED );

-	const ALLOWED_POLICY_TYPES = array( 'percentage', 'fixed_amount', 'price' );
+	public const ALLOWED_POLICY_TYPES = array( 'percentage', 'fixed_amount', 'price' );

 	/**
 	 * Plan id, or null before it is persisted.
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Core/Gateway/GatewayCapabilities.php b/packages/php/woocommerce-subscriptions-engine/src/Core/Gateway/GatewayCapabilities.php
index dd72b6f0479..02cf899ee70 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Core/Gateway/GatewayCapabilities.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Core/Gateway/GatewayCapabilities.php
@@ -34,13 +34,13 @@ final class GatewayCapabilities {
 	 * The most fundamental flag: an engine-scheduled contract on a gateway
 	 * lacking this capability would create renewals nobody runs.
 	 */
-	const RECURRING = 'recurring';
+	public const RECURRING = 'recurring';

 	/**
 	 * Customer can change the payment method on an active contract. All modern
 	 * gateways support this; the flag exists for the rare manual-only case.
 	 */
-	const PAYMENT_METHOD_CHANGE = 'payment_method_change';
+	public const PAYMENT_METHOD_CHANGE = 'payment_method_change';

 	/**
 	 * Gateway tolerates variable charge amounts (volume-tier upgrades, tax-rate
@@ -48,13 +48,13 @@ final class GatewayCapabilities {
 	 * amount-changing flows can refuse on an incapable gateway rather than
 	 * silently mis-charging.
 	 */
-	const AMOUNT_CHANGES = 'amount_changes';
+	public const AMOUNT_CHANGES = 'amount_changes';

 	/**
 	 * One customer can hold N active contracts. Default for modern gateways;
 	 * the flag exists for single-mandate gateways.
 	 */
-	const MULTIPLE_PER_CUSTOMER = 'multiple_per_customer';
+	public const MULTIPLE_PER_CUSTOMER = 'multiple_per_customer';

 	/**
 	 * Gateway schedules and fires renewals itself; the engine only tracks
@@ -62,7 +62,7 @@ final class GatewayCapabilities {
 	 * a gateway can declare this capability without every contract on it being
 	 * gateway-scheduled.
 	 */
-	const GATEWAY_SCHEDULED_RENEWALS = 'gateway_scheduled_renewals';
+	public const GATEWAY_SCHEDULED_RENEWALS = 'gateway_scheduled_renewals';

 	/**
 	 * In-memory declarations, keyed by gateway id => list of declared capability
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Integration/Checkout/OrderLinkage.php b/packages/php/woocommerce-subscriptions-engine/src/Integration/Checkout/OrderLinkage.php
index c1003e13d2e..dd47640f9b2 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Integration/Checkout/OrderLinkage.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Integration/Checkout/OrderLinkage.php
@@ -29,32 +29,32 @@ final class OrderLinkage {
 	 *
 	 * Stored as a stringified integer (order meta is a flat string table).
 	 */
-	const META_CONTRACT_ID = '_subscription_contract_id';
+	public const META_CONTRACT_ID = '_subscription_contract_id';

 	/**
 	 * Order meta key holding the relation type - see the `RELATION_*` constants.
 	 */
-	const META_RELATION_TYPE = '_subscription_relation_type';
+	public const META_RELATION_TYPE = '_subscription_relation_type';

 	/**
 	 * The order whose checkout created the contract (the contract's `origin_order_id`).
 	 */
-	const RELATION_PARENT = 'parent';
+	public const RELATION_PARENT = 'parent';

 	/**
 	 * A renewal order - created by the renewal engine when a cycle bills.
 	 */
-	const RELATION_RENEWAL = 'renewal';
+	public const RELATION_RENEWAL = 'renewal';

 	/**
 	 * A switch order - customer moved between plans.
 	 */
-	const RELATION_SWITCH = 'switch';
+	public const RELATION_SWITCH = 'switch';

 	/**
 	 * A resubscribe order - customer restarted a previously-cancelled contract.
 	 */
-	const RELATION_RESUBSCRIBE = 'resubscribe';
+	public const RELATION_RESUBSCRIBE = 'resubscribe';

 	/**
 	 * All recognized relation types.
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Integration/Contracts/Cancellation.php b/packages/php/woocommerce-subscriptions-engine/src/Integration/Contracts/Cancellation.php
index de28dffc92d..c484fd103ba 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Integration/Contracts/Cancellation.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Integration/Contracts/Cancellation.php
@@ -31,7 +31,7 @@ final class Cancellation {
 	/**
 	 * Action fired after a contract is cancelled, with `( $contract )`.
 	 */
-	const CONTRACT_CANCELLED_ACTION = 'woocommerce_subscriptions_engine_contract_cancelled';
+	public const CONTRACT_CANCELLED_ACTION = 'woocommerce_subscriptions_engine_contract_cancelled';

 	/**
 	 * Contract repository.
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Integration/Gateway/CapabilityRegistry.php b/packages/php/woocommerce-subscriptions-engine/src/Integration/Gateway/CapabilityRegistry.php
index 158e2d3c919..975b974b07c 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Integration/Gateway/CapabilityRegistry.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Integration/Gateway/CapabilityRegistry.php
@@ -40,7 +40,7 @@ final class CapabilityRegistry {
 	 * gateways (where capability resolution depends on the merchant account
 	 * routing the order) flip a capability on or off here per order.
 	 */
-	const CAPABILITY_CHECK_FILTER = 'woocommerce_subscriptions_engine_gateway_capability_check';
+	public const CAPABILITY_CHECK_FILTER = 'woocommerce_subscriptions_engine_gateway_capability_check';

 	/**
 	 * Action fired once capability resolution is stable for the request.
@@ -50,7 +50,7 @@ final class CapabilityRegistry {
 	 * Consumers that resolve capabilities should wait for this action so the
 	 * live-gateway step has a populated registry to read.
 	 */
-	const CAPABILITIES_READY_ACTION = 'woocommerce_subscriptions_engine_capabilities_ready';
+	public const CAPABILITIES_READY_ACTION = 'woocommerce_subscriptions_engine_capabilities_ready';

 	/**
 	 * `woocommerce_loaded` priority for the ready dispatch.
@@ -59,7 +59,7 @@ final class CapabilityRegistry {
 	 * capability flags onto the live gateway instance, so the live-gateway step
 	 * sees those flags by the time the ready action fires.
 	 */
-	const READY_HOOK_PRIORITY = 20;
+	public const READY_HOOK_PRIORITY = 20;

 	/**
 	 * Whether hooks have already been registered, to keep registration
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Integration/Renewal/RenewalEngine.php b/packages/php/woocommerce-subscriptions-engine/src/Integration/Renewal/RenewalEngine.php
index 4652042de0c..bba17af32b2 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Integration/Renewal/RenewalEngine.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Integration/Renewal/RenewalEngine.php
@@ -50,23 +50,23 @@ final class RenewalEngine {
 	 * Action fired after a contract is scheduled, with `( $contract, $when )`.
 	 * Listeners observe a scheduled state, not an in-flight one.
 	 */
-	const RENEWAL_SCHEDULED_ACTION = 'woocommerce_subscriptions_engine_renewal_scheduled';
+	public const RENEWAL_SCHEDULED_ACTION = 'woocommerce_subscriptions_engine_renewal_scheduled';

 	/**
 	 * Action fired after a renewal order is created, with `( $renewal_order, $contract )`.
 	 */
-	const RENEWAL_ORDER_CREATED_ACTION = 'woocommerce_subscriptions_engine_renewal_order_created';
+	public const RENEWAL_ORDER_CREATED_ACTION = 'woocommerce_subscriptions_engine_renewal_order_created';

 	/**
 	 * Action fired after a renewal cycle is billed and the schedule advanced, with
 	 * `( $contract, $cycle, $renewal_order )`.
 	 */
-	const RENEWAL_BILLED_ACTION = 'woocommerce_subscriptions_engine_renewal_billed';
+	public const RENEWAL_BILLED_ACTION = 'woocommerce_subscriptions_engine_renewal_billed';

 	/**
 	 * Logger source tag.
 	 */
-	const LOG_SOURCE = 'woocommerce-subscriptions-engine';
+	protected const LOG_SOURCE = 'woocommerce-subscriptions-engine';

 	/**
 	 * Repository for loading and persisting contracts, and targeted cycle access.
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Integration/Renewal/RenewalScheduler.php b/packages/php/woocommerce-subscriptions-engine/src/Integration/Renewal/RenewalScheduler.php
index 068538acc0c..4233936c96e 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Integration/Renewal/RenewalScheduler.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Integration/Renewal/RenewalScheduler.php
@@ -38,13 +38,13 @@ final class RenewalScheduler {
 	 * Public so tooling and tests can inspect or cancel pending actions via
 	 * `as_has_scheduled_action()` and friends.
 	 */
-	const HOOK = 'woocommerce_subscriptions_engine_process_renewal';
+	public const HOOK = 'woocommerce_subscriptions_engine_process_renewal';

 	/**
 	 * Action Scheduler group - used for admin filterability (Tools ->
 	 * Scheduled Actions) and bulk teardown.
 	 */
-	const GROUP = 'woocommerce_subscriptions_engine';
+	public const GROUP = 'woocommerce_subscriptions_engine';

 	/**
 	 * Enqueue an AS action for `$contract_id` at `$when`.
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Integration/Storage/SchemaInstaller.php b/packages/php/woocommerce-subscriptions-engine/src/Integration/Storage/SchemaInstaller.php
index f9cbb84efa0..5a8f3b55e5d 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Integration/Storage/SchemaInstaller.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Integration/Storage/SchemaInstaller.php
@@ -40,24 +40,24 @@ final class SchemaInstaller {
 	 * on an earlier schema must drop and recreate the tables (and clear VERSION_OPTION)
 	 * to pick up such changes - in-place ALTERs and backfills arrive with the freeze.
 	 */
-	const VERSION = '2.1.1';
+	private const VERSION = '2.1.1';

 	/**
 	 * Option key tracking the installed schema version.
 	 */
-	const VERSION_OPTION = 'wc_subscriptions_engine_db_version';
+	private const VERSION_OPTION = 'wc_subscriptions_engine_db_version';

 	/**
 	 * Logical table identifiers - keys map to unprefixed table names.
 	 */
-	const TABLE_PLAN_GROUPS        = 'plan_groups';
-	const TABLE_PLANS              = 'plans';
-	const TABLE_CONTRACTS          = 'contracts';
-	const TABLE_CONTRACT_ITEMS     = 'contract_items';
-	const TABLE_CONTRACT_ADDRESSES = 'contract_addresses';
-	const TABLE_CONTRACT_META      = 'contract_meta';
-	const TABLE_CYCLES             = 'cycles';
-	const TABLE_SNAPSHOTS          = 'snapshots';
+	public const TABLE_PLAN_GROUPS        = 'plan_groups';
+	public const TABLE_PLANS              = 'plans';
+	public const TABLE_CONTRACTS          = 'contracts';
+	public const TABLE_CONTRACT_ITEMS     = 'contract_items';
+	public const TABLE_CONTRACT_ADDRESSES = 'contract_addresses';
+	public const TABLE_CONTRACT_META      = 'contract_meta';
+	public const TABLE_CYCLES             = 'cycles';
+	public const TABLE_SNAPSHOTS          = 'snapshots';

 	/**
 	 * Resolve a logical identifier to its prefixed table name.
@@ -125,10 +125,28 @@ final class SchemaInstaller {
 	}

 	/**
-	 * Whether the installed schema version matches SchemaInstaller::VERSION.
+	 * Whether the installed schema version matches SchemaInstaller::get_version().
 	 */
 	public static function is_current(): bool {
-		return self::VERSION === get_option( self::VERSION_OPTION );
+		return self::get_version() === self::get_database_version();
+	}
+
+	/**
+	 * Get the schema version for this class.
+	 */
+	public static function get_version(): string {
+		return self::VERSION;
+	}
+
+	/**
+	 * Get the installed schema version from the database.
+	 */
+	public static function get_database_version(): ?string {
+		$database_version = get_option( self::VERSION_OPTION );
+		if ( ! is_string( $database_version ) ) {
+			return null;
+		}
+		return $database_version;
 	}

 	/**
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Integration/Storage/SnapshotStore.php b/packages/php/woocommerce-subscriptions-engine/src/Integration/Storage/SnapshotStore.php
index 0e52a58cf5e..5c74f21719e 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Integration/Storage/SnapshotStore.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Integration/Storage/SnapshotStore.php
@@ -25,8 +25,8 @@ defined( 'ABSPATH' ) || exit;
  */
 final class SnapshotStore {

-	const TYPE_PLAN  = 'plan';
-	const TYPE_ITEMS = 'items';
+	public const TYPE_PLAN  = 'plan';
+	public const TYPE_ITEMS = 'items';

 	/**
 	 * Insert a typed snapshot row and return its id. The payload (the value object's
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Package.php b/packages/php/woocommerce-subscriptions-engine/src/Package.php
index 236adeb6978..7d9a39e303b 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Package.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Package.php
@@ -26,7 +26,7 @@ final class Package {
 	/**
 	 * Package version.
 	 */
-	const VERSION = '0.0.1';
+	protected const VERSION = '0.0.1';

 	/**
 	 * Boot the package's integration layer.
diff --git a/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Storage/SchemaInstallerTest.php b/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Storage/SchemaInstallerTest.php
index 2c42673bffd..43b54a7bcb9 100644
--- a/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Storage/SchemaInstallerTest.php
+++ b/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Storage/SchemaInstallerTest.php
@@ -54,14 +54,14 @@ class SchemaInstallerTest extends EngineIntegrationTestCase {

 	public function test_version_option_is_set_after_install(): void {
 		$this->assertTrue( SchemaInstaller::is_current() );
-		$this->assertSame( SchemaInstaller::VERSION, get_option( SchemaInstaller::VERSION_OPTION ) );
+		$this->assertSame( SchemaInstaller::get_version(), SchemaInstaller::get_database_version() );
 	}

 	public function test_install_is_idempotent(): void {
 		// Running install again must not error or change the recorded version.
 		SchemaInstaller::install();

-		$this->assertSame( SchemaInstaller::VERSION, get_option( SchemaInstaller::VERSION_OPTION ) );
+		$this->assertSame( SchemaInstaller::get_version(), SchemaInstaller::get_database_version() );
 	}

 	public function test_unknown_table_identifier_throws(): void {