Commit 8dbd0dcf1ae for woocommerce

commit 8dbd0dcf1aeef2acd7b2e0eb24a05bb952281da4
Author: Vasily Belolapotkov <vasily.belolapotkov@automattic.com>
Date:   Wed Oct 21 17:57:36 2026 +0200

    Subscriptions engine: remove the BillingPolicy and DeliveryPolicy value objects (#70095)

    Remove subscriptions engine billing and delivery policy value objects

    - Remove Core\ValueObject\BillingPolicy and DeliveryPolicy, which no engine code consumes
    - Remove PlanSnapshot::read_billing_policy(); snapshots still return the captured billing payload
    - Compute the integration fixture's first renewal directly and drop references to the removed classes

diff --git a/packages/php/woocommerce-subscriptions-engine/changelog/update-subscriptions-engine-drop-policy-value-objects b/packages/php/woocommerce-subscriptions-engine/changelog/update-subscriptions-engine-drop-policy-value-objects
new file mode 100644
index 00000000000..2ff067e69d3
--- /dev/null
+++ b/packages/php/woocommerce-subscriptions-engine/changelog/update-subscriptions-engine-drop-policy-value-objects
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Remove the BillingPolicy and DeliveryPolicy value objects and PlanSnapshot::read_billing_policy() from the unreleased subscriptions engine; no changelog entry needed.
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Api/Plans.php b/packages/php/woocommerce-subscriptions-engine/src/Api/Plans.php
index 6f6f4e992a2..3c3f1999f0d 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Api/Plans.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Api/Plans.php
@@ -10,8 +10,7 @@
  * reaches a plan of another one (authorization is the caller's concern). Reads return
  * read-only {@see PlanView}s. The engine opens no transaction and keeps no cache.
  *
- * Plan policies are stored as given; the engine does not read them. An extension may
- * parse a `billing_policy` with {@see \Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\BillingPolicy::from_array()}.
+ * Plan policies are stored as given; the engine does not read them.
  *
  * @package Automattic\WooCommerce\SubscriptionsEngine\Api
  */
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Core/ValueObject/BillingPolicy.php b/packages/php/woocommerce-subscriptions-engine/src/Core/ValueObject/BillingPolicy.php
deleted file mode 100644
index 5ee5a6f9cfc..00000000000
--- a/packages/php/woocommerce-subscriptions-engine/src/Core/ValueObject/BillingPolicy.php
+++ /dev/null
@@ -1,472 +0,0 @@
-<?php
-/**
- * BillingPolicy - an optional parser for a billing cadence and trial payload.
- *
- * An extension may use {@see self::from_array()} to parse its plan billing arrays and
- * compute due moments from them. The engine stores plan policies opaquely and does not
- * read them itself. A policy always has a usable cadence and trial: construction refuses an
- * unknown period or trial unit and a non-positive interval or trial length. The array shape it parses:
- *   {
- *     period:         'day' | 'week' | 'month' | 'year',
- *     interval:       int,
- *     min_cycles:     ?int,
- *     max_cycles:     ?int,
- *     trial_duration: { length: int, unit: 'day'|'week'|'month'|'year' } | null
- *   }
- *
- * Trial is a native field: the first cycle's billing date is delayed by the
- * trial at contract creation rather than modelled as a discount.
- *
- * @package Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject
- */
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject;
-
-use DateTimeImmutable;
-use DateTimeZone;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\Support\Coercion;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\Support\InvalidInputException;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\Support\ValidationErrorCode;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * BillingPolicy value object.
- *
- * Immutable. Construct via {@see self::from_array()} when hydrating from a
- * stored row, or via the constructor when building one in code.
- */
-final class BillingPolicy {
-
-	/**
-	 * Period unit: 'day' | 'week' | 'month' | 'year'.
-	 *
-	 * @var string
-	 */
-	private $period;
-
-	/**
-	 * Period count (e.g. 2 with 'week' = every 2 weeks).
-	 *
-	 * @var int
-	 */
-	private $interval;
-
-	/**
-	 * Minimum cycles before cancellation is allowed; null if unbounded below.
-	 *
-	 * @var int|null
-	 */
-	private $min_cycles;
-
-	/**
-	 * Total cycles before the contract ends; null if open-ended.
-	 *
-	 * @var int|null
-	 */
-	private $max_cycles;
-
-	/**
-	 * Native trial; null if no trial.
-	 *
-	 * @var array{length: int, unit: string}|null
-	 */
-	private $trial_duration;
-
-	/**
-	 * Build a billing policy. Every invalid value is reported in one exception.
-	 *
-	 * @param string                                $period         One of 'day' | 'week' | 'month' | 'year'.
-	 * @param int                                   $interval       Period count.
-	 * @param int|null                              $min_cycles     Minimum cycles before cancellation is allowed.
-	 * @param int|null                              $max_cycles     Total cycles before the contract ends.
-	 * @param array{length: int, unit: string}|null $trial_duration Native trial; null if none.
-	 * @throws InvalidInputException If the period is unknown, the interval is not positive, the cycle bounds or the trial are invalid.
-	 */
-	public function __construct( string $period, int $interval, ?int $min_cycles, ?int $max_cycles, ?array $trial_duration ) {
-		self::throw_found_errors(
-			array_merge(
-				array(
-					self::get_period_error( $period ),
-					self::get_interval_error( $interval ),
-				),
-				self::get_cycle_errors( $min_cycles, $max_cycles ),
-				self::get_trial_errors( $trial_duration )
-			)
-		);
-
-		$this->period         = $period;
-		$this->interval       = $interval;
-		$this->min_cycles     = $min_cycles;
-		$this->max_cycles     = $max_cycles;
-		$this->trial_duration = null === $trial_duration ? null : array(
-			'length' => (int) $trial_duration['length'],
-			'unit'   => (string) $trial_duration['unit'],
-		);
-	}
-
-	/**
-	 * Hydrate from the JSON-decoded `billing_policy` column shape.
-	 *
-	 * Missing nullable keys default to null. `period` and `interval` are required, and
-	 * must form a usable cadence (see the constructor). Every invalid value is reported
-	 * in one exception.
-	 *
-	 * @param array<string, mixed> $data Decoded billing_policy row.
-	 * @throws InvalidInputException If the data is not valid.
-	 */
-	public static function from_array( array $data ): self {
-		$min_cycles = Coercion::coerce_nullable_int( $data['min_cycles'] ?? null );
-		$max_cycles = Coercion::coerce_nullable_int( $data['max_cycles'] ?? null );
-		$trial      = $data['trial_duration'] ?? null;
-
-		self::throw_found_errors(
-			array_merge(
-				array(
-					array_key_exists( 'period', $data )
-						? self::get_period_error( $data['period'] )
-						: new InvalidInputException( ValidationErrorCode::REQUIRED, 'period', 'BillingPolicy: period is required, but not supplied.' ),
-					array_key_exists( 'interval', $data )
-						? self::get_interval_error( $data['interval'] )
-						: new InvalidInputException( ValidationErrorCode::REQUIRED, 'interval', 'BillingPolicy: interval is required, but not supplied.' ),
-				),
-				self::get_cycle_errors( $min_cycles, $max_cycles ),
-				array( self::get_trial_duration_error( $trial ) ),
-				is_array( $trial ) ? self::get_trial_errors( $trial ) : array()
-			)
-		);
-
-		return new self(
-			Coercion::coerce_string( $data['period'] ),
-			Coercion::coerce_int( $data['interval'] ),
-			$min_cycles,
-			$max_cycles,
-			is_array( $trial ) ? array(
-				'length' => Coercion::coerce_int( $trial['length'] ?? null ),
-				'unit'   => Coercion::coerce_string( $trial['unit'] ?? null ),
-			) : null
-		);
-	}
-
-	/**
-	 * Period unit: 'day' | 'week' | 'month' | 'year'.
-	 */
-	public function get_period(): string {
-		return $this->period;
-	}
-
-	/**
-	 * Period count. Together with `period` defines cadence.
-	 */
-	public function get_interval(): int {
-		return $this->interval;
-	}
-
-	/**
-	 * Minimum cycles before cancellation is allowed; null if unbounded below.
-	 */
-	public function get_min_cycles(): ?int {
-		return $this->min_cycles;
-	}
-
-	/**
-	 * Total cycles before the contract ends; null if open-ended.
-	 */
-	public function get_max_cycles(): ?int {
-		return $this->max_cycles;
-	}
-
-	/**
-	 * Native trial duration, or null if no trial.
-	 *
-	 * @return array{length: int, unit: string}|null
-	 */
-	public function get_trial_duration(): ?array {
-		return $this->trial_duration;
-	}
-
-	/**
-	 * Compute the next renewal moment by adding the policy's cadence to `$anchor`.
-	 *
-	 * Trial duration is not applied here. The result is normalized to UTC and
-	 * period semantics are calendar-aware (matching DateTimeImmutable::modify()):
-	 * adding 1 month to 2026-01-31 yields 2026-03-03, not 2026-02-31.
-	 *
-	 * @param DateTimeImmutable $anchor The moment the next cycle is computed from.
-	 * @return DateTimeImmutable The next renewal moment in UTC.
-	 */
-	public function compute_next_renewal_from( DateTimeImmutable $anchor ): DateTimeImmutable {
-		$utc = $anchor->setTimezone( new DateTimeZone( 'UTC' ) );
-
-		return $utc->modify( sprintf( '+%d %s', $this->interval, $this->period ) );
-	}
-
-	/**
-	 * Compute the first renewal moment for a freshly-created contract.
-	 *
-	 * Honours the policy's native trial: when set, the first cycle's billing
-	 * date is the end of the trial. With no trial this delegates to
-	 * {@see self::compute_next_renewal_from()} so there is one cadence-math path.
-	 *
-	 * @param DateTimeImmutable $contract_start Moment the contract was created.
-	 * @return DateTimeImmutable The first renewal moment in UTC.
-	 */
-	public function compute_first_renewal_from( DateTimeImmutable $contract_start ): DateTimeImmutable {
-		if ( null === $this->trial_duration ) {
-			return $this->compute_next_renewal_from( $contract_start );
-		}
-
-		return $contract_start
-			->setTimezone( new DateTimeZone( 'UTC' ) )
-			->modify( sprintf( '+%d %s', $this->trial_duration['length'], $this->trial_duration['unit'] ) );
-	}
-
-	/**
-	 * Serialize back to the JSON column shape. Lossless round-trip with from_array().
-	 *
-	 * @return array<string, mixed>
-	 */
-	public function to_array(): array {
-		return array(
-			'period'         => $this->period,
-			'interval'       => $this->interval,
-			'min_cycles'     => $this->min_cycles,
-			'max_cycles'     => $this->max_cycles,
-			'trial_duration' => $this->trial_duration,
-		);
-	}
-
-	/**
-	 * The error for a unit that is not one of day/week/month/year, or null.
-	 *
-	 * @param string $unit  The raw unit.
-	 * @param string $field Field the unit came from, in dot notation.
-	 */
-	private static function get_unit_error( string $unit, string $field ): ?InvalidInputException {
-		$allowed = array( 'day', 'week', 'month', 'year' );
-		if ( ! in_array( $unit, $allowed, true ) ) {
-			return new InvalidInputException(
-				ValidationErrorCode::NOT_IN_ENUM,
-				$field,
-				sprintf( 'BillingPolicy: invalid %s "%s".', $field, $unit ),
-				array( 'allowed' => $allowed )
-			);
-		}
-
-		return null;
-	}
-
-	/**
-	 * Throw the non-null errors at once; do nothing when there are none.
-	 *
-	 * @param array<int, InvalidInputException|null> $errors Errors, null where a check passed.
-	 * @throws InvalidInputException If an error is present.
-	 */
-	private static function throw_found_errors( array $errors ): void {
-		$errors = array_values( array_filter( $errors ) );
-		if ( array() !== $errors ) {
-			throw InvalidInputException::from_errors( $errors );
-		}
-	}
-
-	/**
-	 * The error for a period that is not a string or not a known unit, or null.
-	 *
-	 * @param mixed $period Candidate period.
-	 */
-	private static function get_period_error( $period ): ?InvalidInputException {
-		if ( ! is_string( $period ) ) {
-			return new InvalidInputException(
-				ValidationErrorCode::INVALID_TYPE,
-				'period',
-				'BillingPolicy: period must be a string, got ' . gettype( $period ) . '.',
-				array( 'type' => 'string' )
-			);
-		}
-
-		return self::get_unit_error( $period, 'period' );
-	}
-
-	/**
-	 * The error for an interval that is not a positive integer, or null.
-	 *
-	 * @param mixed $interval Candidate interval.
-	 */
-	private static function get_interval_error( $interval ): ?InvalidInputException {
-		if ( ! is_int( $interval ) ) {
-			return new InvalidInputException(
-				ValidationErrorCode::INVALID_TYPE,
-				'interval',
-				'BillingPolicy: interval must be an integer, got ' . gettype( $interval ) . '.',
-				array( 'type' => 'integer' )
-			);
-		}
-		if ( $interval <= 0 ) {
-			return new InvalidInputException(
-				ValidationErrorCode::OUT_OF_BOUNDS,
-				'interval',
-				sprintf( 'BillingPolicy: interval must be positive, got %d.', $interval ),
-				array( 'min' => 1 )
-			);
-		}
-
-		return null;
-	}
-
-	/**
-	 * The cycle bound errors: each bound's own, then `min_cycles` above `max_cycles` only
-	 * when both passed theirs.
-	 *
-	 * @param int|null $min_cycles Minimum cycles before cancellation is allowed.
-	 * @param int|null $max_cycles Total cycles before the contract ends.
-	 * @return array<int, InvalidInputException|null>
-	 */
-	private static function get_cycle_errors( ?int $min_cycles, ?int $max_cycles ): array {
-		$min_error = self::get_min_cycles_error( $min_cycles );
-		$max_error = self::get_max_cycles_error( $max_cycles );
-		if ( null !== $min_error || null !== $max_error ) {
-			return array( $min_error, $max_error );
-		}
-
-		return array( self::get_cycle_bounds_error( $min_cycles, $max_cycles ) );
-	}
-
-	/**
-	 * The error for a negative `min_cycles`, or null.
-	 *
-	 * @param int|null $min_cycles Minimum cycles before cancellation is allowed.
-	 */
-	private static function get_min_cycles_error( ?int $min_cycles ): ?InvalidInputException {
-		if ( null !== $min_cycles && $min_cycles < 0 ) {
-			return new InvalidInputException(
-				ValidationErrorCode::OUT_OF_BOUNDS,
-				'min_cycles',
-				sprintf( 'BillingPolicy: min_cycles must be 0 or greater, got %d.', $min_cycles ),
-				array( 'min' => 0 )
-			);
-		}
-
-		return null;
-	}
-
-	/**
-	 * The error for a negative `max_cycles`, or null.
-	 *
-	 * @param int|null $max_cycles Total cycles before the contract ends.
-	 */
-	private static function get_max_cycles_error( ?int $max_cycles ): ?InvalidInputException {
-		if ( null !== $max_cycles && $max_cycles < 0 ) {
-			return new InvalidInputException(
-				ValidationErrorCode::OUT_OF_BOUNDS,
-				'max_cycles',
-				sprintf( 'BillingPolicy: max_cycles must be 0 or greater, got %d.', $max_cycles ),
-				array( 'min' => 0 )
-			);
-		}
-
-		return null;
-	}
-
-	/**
-	 * The error for `min_cycles` above `max_cycles`, or null.
-	 *
-	 * @param int|null $min_cycles Minimum cycles before cancellation is allowed.
-	 * @param int|null $max_cycles Total cycles before the contract ends.
-	 */
-	private static function get_cycle_bounds_error( ?int $min_cycles, ?int $max_cycles ): ?InvalidInputException {
-		if ( null !== $min_cycles && null !== $max_cycles && $min_cycles > $max_cycles ) {
-			return new InvalidInputException(
-				ValidationErrorCode::OUT_OF_BOUNDS,
-				'min_cycles',
-				sprintf( 'BillingPolicy: min_cycles cannot exceed max_cycles, got %d and %d.', $min_cycles, $max_cycles ),
-				array( 'max' => $max_cycles )
-			);
-		}
-
-		return null;
-	}
-
-	/**
-	 * The error for a trial duration that is neither null nor an array, or null.
-	 *
-	 * @param mixed $trial_duration Candidate trial duration.
-	 */
-	private static function get_trial_duration_error( $trial_duration ): ?InvalidInputException {
-		if ( null !== $trial_duration && ! is_array( $trial_duration ) ) {
-			return new InvalidInputException(
-				ValidationErrorCode::INVALID_TYPE,
-				'trial_duration',
-				sprintf( 'BillingPolicy: trial_duration must be null or an array, got %s.', gettype( $trial_duration ) ),
-				array( 'type' => 'object' )
-			);
-		}
-
-		return null;
-	}
-
-	/**
-	 * The trial errors: the length's, then the unit's. None for no trial.
-	 *
-	 * @param array<array-key, mixed>|null $trial_duration Trial duration.
-	 * @return array<int, InvalidInputException|null>
-	 */
-	private static function get_trial_errors( ?array $trial_duration ): array {
-		if ( null === $trial_duration ) {
-			return array();
-		}
-
-		return array( self::get_trial_length_error( $trial_duration ), self::get_trial_unit_error( $trial_duration ) );
-	}
-
-	/**
-	 * The error for a missing, non-integer or non-positive trial length, or null.
-	 *
-	 * @param array<array-key, mixed> $trial_duration Trial duration.
-	 */
-	private static function get_trial_length_error( array $trial_duration ): ?InvalidInputException {
-		if ( ! array_key_exists( 'length', $trial_duration ) ) {
-			return new InvalidInputException( ValidationErrorCode::REQUIRED, 'trial_duration.length', 'BillingPolicy: trial_duration.length is required.' );
-		}
-		if ( ! is_int( $trial_duration['length'] ) ) {
-			return new InvalidInputException(
-				ValidationErrorCode::INVALID_TYPE,
-				'trial_duration.length',
-				sprintf( 'BillingPolicy: trial_duration.length must be an integer, got %s.', gettype( $trial_duration['length'] ) ),
-				array( 'type' => 'integer' )
-			);
-		}
-		if ( $trial_duration['length'] <= 0 ) {
-			return new InvalidInputException(
-				ValidationErrorCode::OUT_OF_BOUNDS,
-				'trial_duration.length',
-				sprintf( 'BillingPolicy: trial_duration.length must be positive, got %d.', $trial_duration['length'] ),
-				array( 'min' => 1 )
-			);
-		}
-
-		return null;
-	}
-
-	/**
-	 * The error for a missing, non-string or unknown trial unit, or null.
-	 *
-	 * @param array<array-key, mixed> $trial_duration Trial duration.
-	 */
-	private static function get_trial_unit_error( array $trial_duration ): ?InvalidInputException {
-		if ( ! array_key_exists( 'unit', $trial_duration ) ) {
-			return new InvalidInputException( ValidationErrorCode::REQUIRED, 'trial_duration.unit', 'BillingPolicy: trial_duration.unit is required.' );
-		}
-		if ( ! is_string( $trial_duration['unit'] ) ) {
-			return new InvalidInputException(
-				ValidationErrorCode::INVALID_TYPE,
-				'trial_duration.unit',
-				sprintf( 'BillingPolicy: trial_duration.unit must be a string, got %s.', gettype( $trial_duration['unit'] ) ),
-				array( 'type' => 'string' )
-			);
-		}
-
-		return self::get_unit_error( $trial_duration['unit'], 'trial_duration.unit' );
-	}
-}
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Core/ValueObject/DeliveryPolicy.php b/packages/php/woocommerce-subscriptions-engine/src/Core/ValueObject/DeliveryPolicy.php
deleted file mode 100644
index 2fed06c187a..00000000000
--- a/packages/php/woocommerce-subscriptions-engine/src/Core/ValueObject/DeliveryPolicy.php
+++ /dev/null
@@ -1,134 +0,0 @@
-<?php
-/**
- * DeliveryPolicy - an optional parser for a delivery anchors, cutoff, and
- * intent payload.
- *
- * An opt-in parser for extensions: an extension may use {@see self::from_array()} to
- * parse its plan delivery arrays. No engine code consumes it yet; the engine stores
- * plan policies opaquely and does not construct this on plan reads or writes.
- * Deliberately thin for now. The array shape it parses:
- *   {
- *     anchors: [{ type: 'MONTHDAY', day: int }, { type: 'YEARDAY', day: int, month: int }, ...],
- *     cutoff:  ?mixed,
- *     intent:  ?mixed
- *   }
- *
- * The shipping/delivery policy parameter set is a new concept still being
- * designed, so `cutoff` and `intent` are passed through verbatim and anchor
- * entries stay as plain associative arrays until a call site needs typed access.
- *
- * Lives in the WordPress-free Core zone.
- *
- * @package Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject
- */
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * DeliveryPolicy value object.
- *
- * Immutable. Construct via {@see self::from_array()} when hydrating from a
- * stored row, or via the constructor when building one in code.
- */
-final class DeliveryPolicy {
-
-	/**
-	 * Anchor entries. Each: `{type, day, month?}`.
-	 *
-	 * @var array<int, array<array-key, mixed>>
-	 */
-	private $anchors;
-
-	/**
-	 * Cutoff window - shape to be designed; passed through verbatim.
-	 *
-	 * @var mixed
-	 */
-	private $cutoff;
-
-	/**
-	 * Delivery intent - shape to be designed; passed through verbatim.
-	 *
-	 * @var mixed
-	 */
-	private $intent;
-
-	/**
-	 * Build a delivery policy.
-	 *
-	 * @param array<int, array<array-key, mixed>> $anchors Anchor entries.
-	 * @param mixed                               $cutoff  Cutoff window.
-	 * @param mixed                               $intent  Delivery intent.
-	 */
-	public function __construct( array $anchors, $cutoff, $intent ) {
-		$this->anchors = $anchors;
-		$this->cutoff  = $cutoff;
-		$this->intent  = $intent;
-	}
-
-	/**
-	 * Hydrate from the JSON-decoded `delivery_policy` column shape.
-	 *
-	 * Missing keys default to safe values - empty array for `anchors`, null for
-	 * `cutoff` and `intent`.
-	 *
-	 * @param array<array-key, mixed> $data Decoded delivery_policy row.
-	 */
-	public static function from_array( array $data ): self {
-		$anchors     = array();
-		$raw_anchors = $data['anchors'] ?? null;
-		if ( is_array( $raw_anchors ) ) {
-			foreach ( $raw_anchors as $anchor ) {
-				if ( is_array( $anchor ) ) {
-					$anchors[] = $anchor;
-				}
-			}
-		}
-
-		return new self( $anchors, $data['cutoff'] ?? null, $data['intent'] ?? null );
-	}
-
-	/**
-	 * Anchor entries describing when in the cycle a charge fires.
-	 *
-	 * @return array<int, array<array-key, mixed>>
-	 */
-	public function get_anchors(): array {
-		return $this->anchors;
-	}
-
-	/**
-	 * Cutoff window. Shape to be designed; returned verbatim.
-	 *
-	 * @return mixed
-	 */
-	public function get_cutoff() {
-		return $this->cutoff;
-	}
-
-	/**
-	 * Delivery intent. Shape to be designed; returned verbatim.
-	 *
-	 * @return mixed
-	 */
-	public function get_intent() {
-		return $this->intent;
-	}
-
-	/**
-	 * Serialize back to the JSON column shape. Lossless round-trip with from_array().
-	 *
-	 * @return array<string, mixed>
-	 */
-	public function to_array(): array {
-		return array(
-			'anchors' => $this->anchors,
-			'cutoff'  => $this->cutoff,
-			'intent'  => $this->intent,
-		);
-	}
-}
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Core/ValueObject/PlanSnapshot.php b/packages/php/woocommerce-subscriptions-engine/src/Core/ValueObject/PlanSnapshot.php
index 5f4b4a3f766..b8177d5358f 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Core/ValueObject/PlanSnapshot.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Core/ValueObject/PlanSnapshot.php
@@ -17,7 +17,6 @@ declare( strict_types=1 );
 namespace Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject;

 use LogicException;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\Support\InvalidInputException;
 use Automattic\WooCommerce\SubscriptionsEngine\Core\Support\Coercion;

 defined( 'ABSPATH' ) || exit;
@@ -101,23 +100,6 @@ final class PlanSnapshot {
 		return isset( $this->data['selling_plan_id'] ) ? Coercion::coerce_int( $this->data['selling_plan_id'] ) : null;
 	}

-	/**
-	 * The frozen billing cadence captured at signup, parsed with
-	 * {@see BillingPolicy::from_array()}, so it holds after the source plan is edited or deleted. Null when the payload carries no billing
-	 * policy array; throws when one is present but unusable, so a caller can log why
-	 * before falling back.
-	 *
-	 * @throws InvalidInputException If the stored policy does not parse or has no usable cadence.
-	 */
-	public function read_billing_policy(): ?BillingPolicy {
-		$policy = $this->data['billing_policy'] ?? null;
-		if ( ! is_array( $policy ) ) {
-			return null;
-		}
-
-		return BillingPolicy::from_array( Coercion::coerce_string_keyed( $policy ) );
-	}
-
 	/**
 	 * The frozen pricing payload captured at signup, returned as captured. The
 	 * owning extension interprets it. Null when absent, explicitly null, or not an
diff --git a/packages/php/woocommerce-subscriptions-engine/tests/integration/EngineIntegrationTestCase.php b/packages/php/woocommerce-subscriptions-engine/tests/integration/EngineIntegrationTestCase.php
index d15707aa4ea..bcc01a3ccdb 100644
--- a/packages/php/woocommerce-subscriptions-engine/tests/integration/EngineIntegrationTestCase.php
+++ b/packages/php/woocommerce-subscriptions-engine/tests/integration/EngineIntegrationTestCase.php
@@ -20,7 +20,6 @@ use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\Contract;
 use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\ContractStatus;
 use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\Cycle;
 use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\CycleStatus;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\BillingPolicy;
 use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\ContractLockState;
 use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\ContractRepository;
 use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\SchemaInstaller;
@@ -435,6 +434,9 @@ abstract class EngineIntegrationTestCase extends WP_UnitTestCase {
 			? new DateTimeImmutable( '@' . $paid->getTimestamp() )
 			: new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) );

+		// The engine does not read the plan's cadence, so the fixture bills monthly like make_plan().
+		$next_payment = $start->setTimezone( new DateTimeZone( 'UTC' ) )->modify( '+1 month' );
+
 		$items = array();
 		foreach ( $order->get_items() as $item ) {
 			if ( $item instanceof WC_Order_Item_Product ) {
@@ -465,7 +467,7 @@ abstract class EngineIntegrationTestCase extends WP_UnitTestCase {
 				'payment_method_title' => '' !== $order->get_payment_method_title() ? $order->get_payment_method_title() : null,
 				'payment_token_id'     => $token_id > 0 ? $token_id : null,
 				'start_gmt'            => $start,
-				'next_payment_gmt'     => BillingPolicy::from_array( $plan->get_billing_policy() ?? array() )->compute_first_renewal_from( $start ),
+				'next_payment_gmt'     => $next_payment,
 				'billing_total'        => (string) $order->get_total(),
 				'discount_total'       => (string) $order->get_total_discount(),
 				'shipping_total'       => (string) $order->get_shipping_total(),
diff --git a/packages/php/woocommerce-subscriptions-engine/tests/unit/Core/Support/InvalidInputExceptionTest.php b/packages/php/woocommerce-subscriptions-engine/tests/unit/Core/Support/InvalidInputExceptionTest.php
index 7b8c8d86b35..e957704b653 100644
--- a/packages/php/woocommerce-subscriptions-engine/tests/unit/Core/Support/InvalidInputExceptionTest.php
+++ b/packages/php/woocommerce-subscriptions-engine/tests/unit/Core/Support/InvalidInputExceptionTest.php
@@ -30,7 +30,7 @@ class InvalidInputExceptionTest extends TestCase {
 			throw new InvalidInputException(
 				ValidationErrorCode::OUT_OF_BOUNDS,
 				'trial_duration.length',
-				'BillingPolicy: trial length must be positive.',
+				'Trial length must be positive.',
 				array( 'min' => 1 )
 			);
 		} catch ( DomainException $caught ) {
@@ -41,7 +41,7 @@ class InvalidInputExceptionTest extends TestCase {
 		$this->assertSame( 'out_of_bounds', $e->get_error_code() );
 		$this->assertSame( 'trial_duration.length', $e->get_field() );
 		$this->assertSame( array( 'min' => 1 ), $e->get_params() );
-		$this->assertSame( 'BillingPolicy: trial length must be positive.', $e->getMessage() );
+		$this->assertSame( 'Trial length must be positive.', $e->getMessage() );
 	}

 	/**
diff --git a/packages/php/woocommerce-subscriptions-engine/tests/unit/Core/ValueObject/BillingPolicyTest.php b/packages/php/woocommerce-subscriptions-engine/tests/unit/Core/ValueObject/BillingPolicyTest.php
deleted file mode 100644
index c727ec96aca..00000000000
--- a/packages/php/woocommerce-subscriptions-engine/tests/unit/Core/ValueObject/BillingPolicyTest.php
+++ /dev/null
@@ -1,573 +0,0 @@
-<?php
-/**
- * Unit tests for BillingPolicy.
- *
- * @package Automattic\WooCommerce\SubscriptionsEngine
- */
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\SubscriptionsEngine\Tests\Unit\Core\ValueObject;
-
-use DateTimeImmutable;
-use DateTimeZone;
-use PHPUnit\Framework\TestCase;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\Support\InvalidInputException;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\Support\ValidationErrorCode;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\BillingPolicy;
-use Automattic\WooCommerce\SubscriptionsEngine\Tests\Unit\AssertsInvalidInput;
-
-/**
- * @covers \Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\BillingPolicy
- */
-class BillingPolicyTest extends TestCase {
-
-	use AssertsInvalidInput;
-
-	/**
-	 * @testdox Each refusal reports its code, field and params.
-	 * @dataProvider provide_refusals
-	 *
-	 * @param callable             $operation Operation expected to be refused.
-	 * @param string               $code      Expected error code.
-	 * @param string               $field     Expected field.
-	 * @param array<string, mixed> $params    Expected params.
-	 */
-	public function test_each_refusal_reports_its_code_field_and_params( callable $operation, string $code, string $field, array $params ): void {
-		$this->assert_invalid_input( $operation, $code, $field, $params );
-	}
-
-	/**
-	 * @return array<string, array{0: callable, 1: string, 2: string, 3: array<string, mixed>}>
-	 */
-	public function provide_refusals(): array {
-		$units = array( 'allowed' => array( 'day', 'week', 'month', 'year' ) );
-
-		return array(
-			'interval not positive'     => array(
-				static function (): void {
-					new BillingPolicy( 'month', 0, null, null, null );
-				},
-				ValidationErrorCode::OUT_OF_BOUNDS,
-				'interval',
-				array( 'min' => 1 ),
-			),
-			'unknown period'            => array(
-				static function (): void {
-					new BillingPolicy( 'fortnight', 1, null, null, null );
-				},
-				ValidationErrorCode::NOT_IN_ENUM,
-				'period',
-				$units,
-			),
-			'period missing'            => array(
-				static function (): void {
-					BillingPolicy::from_array( array( 'interval' => 1 ) );
-				},
-				ValidationErrorCode::REQUIRED,
-				'period',
-				array(),
-			),
-			'interval missing'          => array(
-				static function (): void {
-					BillingPolicy::from_array( array( 'period' => 'month' ) );
-				},
-				ValidationErrorCode::REQUIRED,
-				'interval',
-				array(),
-			),
-			'period not a string'       => array(
-				static function (): void {
-					BillingPolicy::from_array(
-						array(
-							'period'   => 1,
-							'interval' => 1,
-						)
-					);
-				},
-				ValidationErrorCode::INVALID_TYPE,
-				'period',
-				array( 'type' => 'string' ),
-			),
-			'interval not an integer'   => array(
-				static function (): void {
-					BillingPolicy::from_array(
-						array(
-							'period'   => 'month',
-							'interval' => '2',
-						)
-					);
-				},
-				ValidationErrorCode::INVALID_TYPE,
-				'interval',
-				array( 'type' => 'integer' ),
-			),
-			'trial not an object'       => array(
-				static function (): void {
-					BillingPolicy::from_array(
-						array(
-							'period'         => 'month',
-							'interval'       => 1,
-							'trial_duration' => 'P7D',
-						)
-					);
-				},
-				ValidationErrorCode::INVALID_TYPE,
-				'trial_duration',
-				array( 'type' => 'object' ),
-			),
-			'min_cycles negative'       => array(
-				static function (): void {
-					new BillingPolicy( 'month', 1, -1, null, null );
-				},
-				ValidationErrorCode::OUT_OF_BOUNDS,
-				'min_cycles',
-				array( 'min' => 0 ),
-			),
-			'max_cycles negative'       => array(
-				static function (): void {
-					new BillingPolicy( 'month', 1, null, -1, null );
-				},
-				ValidationErrorCode::OUT_OF_BOUNDS,
-				'max_cycles',
-				array( 'min' => 0 ),
-			),
-			'min_cycles above max'      => array(
-				static function (): void {
-					new BillingPolicy( 'month', 1, 5, 2, null );
-				},
-				ValidationErrorCode::OUT_OF_BOUNDS,
-				'min_cycles',
-				array( 'max' => 2 ),
-			),
-			'trial length missing'      => array(
-				static function (): void {
-					BillingPolicy::from_array(
-						array(
-							'period'         => 'month',
-							'interval'       => 1,
-							'trial_duration' => array( 'unit' => 'day' ),
-						)
-					);
-				},
-				ValidationErrorCode::REQUIRED,
-				'trial_duration.length',
-				array(),
-			),
-			'trial unit missing'        => array(
-				static function (): void {
-					BillingPolicy::from_array(
-						array(
-							'period'         => 'month',
-							'interval'       => 1,
-							'trial_duration' => array( 'length' => 7 ),
-						)
-					);
-				},
-				ValidationErrorCode::REQUIRED,
-				'trial_duration.unit',
-				array(),
-			),
-			'trial length not integer'  => array(
-				static function (): void {
-					BillingPolicy::from_array(
-						array(
-							'period'         => 'month',
-							'interval'       => 1,
-							'trial_duration' => array(
-								'length' => '7',
-								'unit'   => 'day',
-							),
-						)
-					);
-				},
-				ValidationErrorCode::INVALID_TYPE,
-				'trial_duration.length',
-				array( 'type' => 'integer' ),
-			),
-			'trial unit not a string'   => array(
-				static function (): void {
-					BillingPolicy::from_array(
-						array(
-							'period'         => 'month',
-							'interval'       => 1,
-							'trial_duration' => array(
-								'length' => 7,
-								'unit'   => 1,
-							),
-						)
-					);
-				},
-				ValidationErrorCode::INVALID_TYPE,
-				'trial_duration.unit',
-				array( 'type' => 'string' ),
-			),
-			'trial length not positive' => array(
-				static function (): void {
-					new BillingPolicy(
-						'month',
-						1,
-						null,
-						null,
-						array(
-							'length' => 0,
-							'unit'   => 'day',
-						)
-					);
-				},
-				ValidationErrorCode::OUT_OF_BOUNDS,
-				'trial_duration.length',
-				array( 'min' => 1 ),
-			),
-			'unknown trial unit'        => array(
-				static function (): void {
-					new BillingPolicy(
-						'month',
-						1,
-						null,
-						null,
-						array(
-							'length' => 7,
-							'unit'   => 'fortnight',
-						)
-					);
-				},
-				ValidationErrorCode::NOT_IN_ENUM,
-				'trial_duration.unit',
-				$units,
-			),
-		);
-	}
-
-	/**
-	 * @testdox from_array refuses an unusable cadence.
-	 * @testWith ["decade", 1, "BillingPolicy: invalid period \"decade\"."]
-	 *           ["month", 0, "BillingPolicy: interval must be positive, got 0."]
-	 *           ["day", -1, "BillingPolicy: interval must be positive, got -1."]
-	 *
-	 * @param string $period   Period.
-	 * @param int    $interval Interval.
-	 * @param string $message  Expected exception message.
-	 */
-	public function test_from_array_refuses_an_unusable_cadence( string $period, int $interval, string $message ): void {
-		$data = array(
-			'period'   => $period,
-			'interval' => $interval,
-		);
-
-		$this->expectException( InvalidInputException::class );
-		$this->expectExceptionMessage( $message );
-
-		BillingPolicy::from_array( $data );
-	}
-
-	/**
-	 * @testdox from_array accepts a usable cadence.
-	 */
-	public function test_from_array_accepts_a_usable_cadence(): void {
-		$policy = BillingPolicy::from_array(
-			array(
-				'period'   => 'week',
-				'interval' => 2,
-			)
-		);
-
-		$this->assertSame( 'week', $policy->get_period() );
-		$this->assertSame( 2, $policy->get_interval() );
-	}
-
-	public function test_round_trips_through_array(): void {
-		$data = array(
-			'period'         => 'month',
-			'interval'       => 2,
-			'min_cycles'     => 1,
-			'max_cycles'     => 12,
-			'trial_duration' => array(
-				'length' => 14,
-				'unit'   => 'day',
-			),
-		);
-
-		$policy = BillingPolicy::from_array( $data );
-
-		$this->assertSame( 'month', $policy->get_period() );
-		$this->assertSame( 2, $policy->get_interval() );
-		$this->assertSame( 1, $policy->get_min_cycles() );
-		$this->assertSame( 12, $policy->get_max_cycles() );
-		$this->assertSame( $data['trial_duration'], $policy->get_trial_duration() );
-		$this->assertSame( $data, $policy->to_array() );
-	}
-
-	public function test_missing_nullable_keys_default_to_null(): void {
-		$policy = BillingPolicy::from_array(
-			array(
-				'period'   => 'week',
-				'interval' => 1,
-			)
-		);
-
-		$this->assertNull( $policy->get_min_cycles() );
-		$this->assertNull( $policy->get_max_cycles() );
-		$this->assertNull( $policy->get_trial_duration() );
-	}
-
-	public function test_compute_next_renewal_adds_one_cadence_in_utc(): void {
-		$policy = BillingPolicy::from_array(
-			array(
-				'period'   => 'month',
-				'interval' => 1,
-			)
-		);
-
-		$anchor = new DateTimeImmutable( '2026-01-15 10:00:00', new DateTimeZone( 'UTC' ) );
-		$next   = $policy->compute_next_renewal_from( $anchor );
-
-		$this->assertSame( '2026-02-15 10:00:00', $next->format( 'Y-m-d H:i:s' ) );
-		$this->assertSame( 'UTC', $next->getTimezone()->getName() );
-	}
-
-	public function test_compute_first_renewal_honours_trial(): void {
-		$policy = BillingPolicy::from_array(
-			array(
-				'period'         => 'month',
-				'interval'       => 1,
-				'trial_duration' => array(
-					'length' => 7,
-					'unit'   => 'day',
-				),
-			)
-		);
-
-		$start = new DateTimeImmutable( '2026-01-01 00:00:00', new DateTimeZone( 'UTC' ) );
-		$first = $policy->compute_first_renewal_from( $start );
-
-		$this->assertSame( '2026-01-08 00:00:00', $first->format( 'Y-m-d H:i:s' ) );
-	}
-
-	public function test_compute_first_renewal_without_trial_matches_next(): void {
-		$policy = BillingPolicy::from_array(
-			array(
-				'period'   => 'year',
-				'interval' => 1,
-			)
-		);
-
-		$start = new DateTimeImmutable( '2026-03-10 12:00:00', new DateTimeZone( 'UTC' ) );
-
-		$this->assertEquals(
-			$policy->compute_next_renewal_from( $start ),
-			$policy->compute_first_renewal_from( $start )
-		);
-	}
-
-	/**
-	 * @testdox from_array reports every invalid value in one exception.
-	 */
-	public function test_from_array_reports_every_invalid_value(): void {
-		$this->assert_invalid_inputs(
-			static function (): void {
-				BillingPolicy::from_array(
-					array(
-						'interval'       => 0,
-						'min_cycles'     => -1,
-						'trial_duration' => array( 'length' => 'x' ),
-					)
-				);
-			},
-			array(
-				array( ValidationErrorCode::REQUIRED, 'period', array() ),
-				array( ValidationErrorCode::OUT_OF_BOUNDS, 'interval', array( 'min' => 1 ) ),
-				array( ValidationErrorCode::OUT_OF_BOUNDS, 'min_cycles', array( 'min' => 0 ) ),
-				array( ValidationErrorCode::INVALID_TYPE, 'trial_duration.length', array( 'type' => 'integer' ) ),
-				array( ValidationErrorCode::REQUIRED, 'trial_duration.unit', array() ),
-			)
-		);
-	}
-
-	/**
-	 * @testdox from_array reports a bad period, trial length and trial unit together.
-	 */
-	public function test_from_array_reports_the_trial_values_with_the_other_fields(): void {
-		$this->assert_invalid_inputs(
-			static function (): void {
-				BillingPolicy::from_array(
-					array(
-						'period'         => 'fortnight',
-						'interval'       => 1,
-						'trial_duration' => array(
-							'length' => 0,
-							'unit'   => 'fortnight',
-						),
-					)
-				);
-			},
-			array(
-				array( ValidationErrorCode::NOT_IN_ENUM, 'period', array( 'allowed' => array( 'day', 'week', 'month', 'year' ) ) ),
-				array( ValidationErrorCode::OUT_OF_BOUNDS, 'trial_duration.length', array( 'min' => 1 ) ),
-				array( ValidationErrorCode::NOT_IN_ENUM, 'trial_duration.unit', array( 'allowed' => array( 'day', 'week', 'month', 'year' ) ) ),
-			)
-		);
-	}
-
-	/**
-	 * @testdox The construction reports every invalid value in one exception.
-	 */
-	public function test_construction_reports_every_invalid_value(): void {
-		$this->assert_invalid_inputs(
-			static function (): void {
-				new BillingPolicy( 'fortnight', 0, null, -2, null );
-			},
-			array(
-				array( ValidationErrorCode::NOT_IN_ENUM, 'period', array( 'allowed' => array( 'day', 'week', 'month', 'year' ) ) ),
-				array( ValidationErrorCode::OUT_OF_BOUNDS, 'interval', array( 'min' => 1 ) ),
-				array( ValidationErrorCode::OUT_OF_BOUNDS, 'max_cycles', array( 'min' => 0 ) ),
-			)
-		);
-	}
-
-	/**
-	 * @testdox min_cycles above max_cycles is checked only when both bounds pass their own checks.
-	 */
-	public function test_the_cycle_bounds_check_runs_only_on_valid_bounds(): void {
-		$this->assert_invalid_inputs(
-			static function (): void {
-				new BillingPolicy( 'month', 1, 5, 2, null );
-			},
-			array( array( ValidationErrorCode::OUT_OF_BOUNDS, 'min_cycles', array( 'max' => 2 ) ) )
-		);
-		$this->assert_invalid_inputs(
-			static function (): void {
-				new BillingPolicy( 'month', 1, -1, 2, null );
-			},
-			array( array( ValidationErrorCode::OUT_OF_BOUNDS, 'min_cycles', array( 'min' => 0 ) ) )
-		);
-	}
-
-	public function test_non_array_trial_duration_throws(): void {
-		$this->expectException( InvalidInputException::class );
-		$this->expectExceptionMessage( 'BillingPolicy: trial_duration must be null or an array, got string.' );
-
-		BillingPolicy::from_array(
-			array(
-				'period'         => 'month',
-				'interval'       => 1,
-				'trial_duration' => 'P7D',
-			)
-		);
-	}
-
-	/**
-	 * @dataProvider provide_min_and_max_cycles_validation_cases
-	 * @param string|null $expected_exception_message The expected exception message, or null if no exception is expected.
-	 * @param int|null    $min_cycles                 The minimum number of cycles.
-	 * @param int|null    $max_cycles                 The maximum number of cycles.
-	 */
-	public function test_min_and_max_cycles_validation( ?string $expected_exception_message, ?int $min_cycles, ?int $max_cycles ): void {
-		if ( null !== $expected_exception_message ) {
-			$this->expectException( InvalidInputException::class );
-			$this->expectExceptionMessage( $expected_exception_message );
-		}
-
-		$policy = BillingPolicy::from_array(
-			array(
-				'period'     => 'month',
-				'interval'   => 1,
-				'min_cycles' => $min_cycles,
-				'max_cycles' => $max_cycles,
-			)
-		);
-
-		if ( null === $expected_exception_message ) {
-			$this->assertSame( $min_cycles, $policy->get_min_cycles() );
-			$this->assertSame( $max_cycles, $policy->get_max_cycles() );
-		}
-	}
-
-	/**
-	 * @return array<string, array{expected_exception_message: string|null, min_cycles: int|null, max_cycles: int|null}>
-	 */
-	public function provide_min_and_max_cycles_validation_cases(): array {
-		return array(
-			'min_cycles is 0, max_cycles is null'        => array(
-				'expected_exception_message' => null,
-				'min_cycles'                 => 0,
-				'max_cycles'                 => null,
-			),
-			'min_cycles is 0, max_cycles is positive'    => array(
-				'expected_exception_message' => null,
-				'min_cycles'                 => 0,
-				'max_cycles'                 => 10,
-			),
-			'min_cycles is 0, max_cycles is less than 0' => array(
-				'expected_exception_message' => 'BillingPolicy: max_cycles must be 0 or greater, got -4.',
-				'min_cycles'                 => 0,
-				'max_cycles'                 => -4,
-			),
-			'max_cycles is 0, min_cycles is null'        => array(
-				'expected_exception_message' => null,
-				'min_cycles'                 => null,
-				'max_cycles'                 => 0,
-			),
-			'max_cycles is 0, min_cycles is positive'    => array(
-				'expected_exception_message' => 'BillingPolicy: min_cycles cannot exceed max_cycles, got 5 and 0.',
-				'min_cycles'                 => 5,
-				'max_cycles'                 => 0,
-			),
-			'max_cycles is 0, min_cycles is greater than max_cycles' => array(
-				'expected_exception_message' => 'BillingPolicy: min_cycles cannot exceed max_cycles, got 5 and 0.',
-				'min_cycles'                 => 5,
-				'max_cycles'                 => 0,
-			),
-			'max_cycles is positive, min_cycles is null' => array(
-				'expected_exception_message' => null,
-				'min_cycles'                 => null,
-				'max_cycles'                 => 10,
-			),
-			'max_cycles is positive, min_cycles is positive' => array(
-				'expected_exception_message' => null,
-				'min_cycles'                 => 1,
-				'max_cycles'                 => 10,
-			),
-			'max_cycles is positive, min_cycles is the same as max_cycles' => array(
-				'expected_exception_message' => null,
-				'min_cycles'                 => 10,
-				'max_cycles'                 => 10,
-			),
-			'min_cycles is positive, max_cycles is null' => array(
-				'expected_exception_message' => null,
-				'min_cycles'                 => 1,
-				'max_cycles'                 => null,
-			),
-			'min_cycles is positive, max_cycles is positive' => array(
-				'expected_exception_message' => null,
-				'min_cycles'                 => 1,
-				'max_cycles'                 => 10,
-			),
-			'min_cycles is positive, max_cycles is less than min_cycles' => array(
-				'expected_exception_message' => 'BillingPolicy: min_cycles cannot exceed max_cycles, got 10 and 9.',
-				'min_cycles'                 => 10,
-				'max_cycles'                 => 9,
-			),
-			'min_cycles is negative, max_cycles is null' => array(
-				'expected_exception_message' => 'BillingPolicy: min_cycles must be 0 or greater, got -1.',
-				'min_cycles'                 => -1,
-				'max_cycles'                 => null,
-			),
-			'min_cycles is negative, max_cycles is positive' => array(
-				'expected_exception_message' => 'BillingPolicy: min_cycles must be 0 or greater, got -1.',
-				'min_cycles'                 => -1,
-				'max_cycles'                 => 10,
-			),
-			'min_cycles is negative, max_cycles is less than min_cycles' => array(
-				'expected_exception_message' => 'BillingPolicy: min_cycles must be 0 or greater, got -1.',
-				'min_cycles'                 => -1,
-				'max_cycles'                 => -1,
-			),
-			'min_cycles is positive, max_cycles is negative' => array(
-				'expected_exception_message' => 'BillingPolicy: max_cycles must be 0 or greater, got -1.',
-				'min_cycles'                 => 1,
-				'max_cycles'                 => -1,
-			),
-		);
-	}
-}
diff --git a/packages/php/woocommerce-subscriptions-engine/tests/unit/Core/ValueObject/PlanSnapshotTest.php b/packages/php/woocommerce-subscriptions-engine/tests/unit/Core/ValueObject/PlanSnapshotTest.php
index 384c456d33d..56a305711d4 100644
--- a/packages/php/woocommerce-subscriptions-engine/tests/unit/Core/ValueObject/PlanSnapshotTest.php
+++ b/packages/php/woocommerce-subscriptions-engine/tests/unit/Core/ValueObject/PlanSnapshotTest.php
@@ -10,8 +10,6 @@ declare( strict_types=1 );
 namespace Automattic\WooCommerce\SubscriptionsEngine\Tests\Unit\Core\ValueObject;

 use PHPUnit\Framework\TestCase;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\Support\InvalidInputException;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\BillingPolicy;
 use Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\PlanSnapshot;
 use Automattic\WooCommerce\SubscriptionsEngine\Tests\Unit\AssertsInternalInvariant;

@@ -101,80 +99,6 @@ class PlanSnapshotTest extends TestCase {
 		);
 	}

-	/**
-	 * @testdox read_billing_policy reconstructs the frozen cadence from the payload.
-	 */
-	public function test_read_billing_policy_reconstructs_the_frozen_cadence(): void {
-		$snapshot = PlanSnapshot::from_array(
-			array(
-				'selling_plan_id' => 7,
-				'name'            => 'Monthly box',
-				'billing_policy'  => array(
-					'period'         => 'month',
-					'interval'       => 3,
-					'min_cycles'     => null,
-					'max_cycles'     => 12,
-					'trial_duration' => null,
-				),
-			)
-		);
-
-		$policy = $snapshot->read_billing_policy();
-
-		$this->assertInstanceOf( BillingPolicy::class, $policy );
-		$this->assertSame( 'month', $policy->get_period() );
-		$this->assertSame( 3, $policy->get_interval() );
-		$this->assertSame( 12, $policy->get_max_cycles() );
-	}
-
-	/**
-	 * @testdox read_billing_policy throws for a structurally-invalid stored policy.
-	 */
-	public function test_read_billing_policy_throws_for_an_unreadable_policy(): void {
-		// `interval` missing: BillingPolicy::from_array() refuses it.
-		$snapshot = PlanSnapshot::from_array(
-			array(
-				'billing_policy' => array( 'period' => 'month' ),
-			)
-		);
-
-		$this->expectException( InvalidInputException::class );
-		$snapshot->read_billing_policy();
-	}
-
-	/**
-	 * @testdox read_billing_policy throws for a policy with no usable cadence ($label).
-	 *
-	 * @testWith ["unknown period", "decade", 1]
-	 *           ["zero interval", "month", 0]
-	 *           ["negative interval", "week", -2]
-	 *
-	 * @param string $label    Case label.
-	 * @param string $period   Stored period.
-	 * @param int    $interval Stored interval.
-	 */
-	public function test_a_policy_without_a_usable_cadence_is_not_read( string $label, string $period, int $interval ): void {
-		unset( $label );
-		$snapshot = PlanSnapshot::from_array(
-			array(
-				'billing_policy' => array(
-					'period'   => $period,
-					'interval' => $interval,
-				),
-			)
-		);
-
-		$this->expectException( InvalidInputException::class );
-		$snapshot->read_billing_policy();
-	}
-
-	/**
-	 * @testdox read_billing_policy is null when the payload carries no billing policy.
-	 */
-	public function test_read_billing_policy_is_null_when_absent(): void {
-		$this->assertNull( PlanSnapshot::from_array( array( 'selling_plan_id' => 7 ) )->read_billing_policy() );
-	}
-
 	/**
 	 * @testdox get_pricing_policy returns the frozen payload exactly as captured.
 	 */