Commit 78ac7113e3a for woocommerce
commit 78ac7113e3a06c7af9efe37286ad7df4dd82697e
Author: Vasily Belolapotkov <vasily.belolapotkov@automattic.com>
Date: Tue Jul 7 16:45:36 2026 +0200
Flatten the selling-plans catalog: remove plan groups (#66313)
- Schema 2.3.0: drop the plan_groups table and the plans group_id/options columns; merchant_code moves onto plans with a per-extension UNIQUE (extension_slug, merchant_code) - pre-release drop/recreate, no migration
- Delete PlanGroup and PlanGroupRepository; Plan::create() takes a single args array, with merchant_code immutable post-create
- Stop auto-creating wrapper groups in PlansController and drop the group object from REST responses; merchant_code stays internal
- Surface failed plan updates as a 500 error instead of returning 200 with the in-memory plan
diff --git a/packages/php/woocommerce-subscriptions-engine/changelog/fix-subscriptions-engine-plan-group-column-name b/packages/php/woocommerce-subscriptions-engine/changelog/fix-subscriptions-engine-plan-group-column-name
deleted file mode 100644
index a76434e6a0b..00000000000
--- a/packages/php/woocommerce-subscriptions-engine/changelog/fix-subscriptions-engine-plan-group-column-name
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: major
-Type: dev
-
-Rename app_id column to extension slug for plan groups
diff --git a/packages/php/woocommerce-subscriptions-engine/changelog/flatten-selling-plans-catalog b/packages/php/woocommerce-subscriptions-engine/changelog/flatten-selling-plans-catalog
new file mode 100644
index 00000000000..1e218d8ceca
--- /dev/null
+++ b/packages/php/woocommerce-subscriptions-engine/changelog/flatten-selling-plans-catalog
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Flatten the selling-plans catalog: remove plan groups; plans carry merchant_code directly.
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Api/Rest/PlansController.php b/packages/php/woocommerce-subscriptions-engine/src/Api/Rest/PlansController.php
index 67e1f4029e5..ff0591dce2b 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Api/Rest/PlansController.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Api/Rest/PlansController.php
@@ -10,11 +10,9 @@ declare( strict_types=1 );
namespace Automattic\WooCommerce\SubscriptionsEngine\Api\Rest;
use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\Plan;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\PlanGroup;
use Automattic\WooCommerce\SubscriptionsEngine\Core\Support\ScalarCoercion;
use Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\BillingPolicy;
use Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\PricingPolicy;
-use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanGroupRepository;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanRepository;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Support\RESTPermissions;
use InvalidArgumentException;
@@ -47,13 +45,6 @@ final class PlansController extends WP_REST_Controller {
*/
private $plan_repository;
- /**
- * Plan groups repository.
- *
- * @var PlanGroupRepository
- */
- private $plan_group_repository;
-
/**
* REST permissions.
*
@@ -64,16 +55,14 @@ final class PlansController extends WP_REST_Controller {
/**
* Construct the controller.
*
- * @param PlanRepository|null $plan_repository Plans repository.
- * @param PlanGroupRepository|null $plan_group_repository Plan groups repository.
- * @param RESTPermissions|null $rest_permissions REST permissions.
+ * @param PlanRepository|null $plan_repository Plans repository.
+ * @param RESTPermissions|null $rest_permissions REST permissions.
*/
- public function __construct( ?PlanRepository $plan_repository = null, ?PlanGroupRepository $plan_group_repository = null, ?RESTPermissions $rest_permissions = null ) {
- $this->namespace = self::REST_NAMESPACE;
- $this->rest_base = self::REST_BASE;
- $this->plan_repository = $plan_repository ?? new PlanRepository();
- $this->plan_group_repository = $plan_group_repository ?? new PlanGroupRepository();
- $this->rest_permissions = $rest_permissions ?? new RESTPermissions();
+ public function __construct( ?PlanRepository $plan_repository = null, ?RESTPermissions $rest_permissions = null ) {
+ $this->namespace = self::REST_NAMESPACE;
+ $this->rest_base = self::REST_BASE;
+ $this->plan_repository = $plan_repository ?? new PlanRepository();
+ $this->rest_permissions = $rest_permissions ?? new RESTPermissions();
}
/**
@@ -293,32 +282,19 @@ final class PlansController extends WP_REST_Controller {
try {
$billing_policy = $this->associative_array( $billing_policy, 'billing_policy must be an object.' );
- $plan_args = array(
- 'name' => $name,
- 'description' => $this->nullable_string_param( $request, 'description' ),
- 'options' => array(),
- 'billing_policy' => BillingPolicy::from_array( $billing_policy ),
- 'pricing_policy' => $this->pricing_policy_from_param( $request->get_param( 'pricing_policy' ), null ),
- 'category' => $this->string_param( $request, 'category', Plan::DEFAULT_CATEGORY ),
- 'status' => $this->string_param( $request, 'status', Plan::STATUS_ACTIVE ),
- 'sort_order' => ScalarCoercion::coerce_int( $request->get_param( 'sort_order' ) ),
- 'extension_slug' => $extension_slug,
- );
- Plan::create( 0, $plan_args );
- $group = PlanGroup::create(
+ $plan = Plan::create(
array(
- 'name' => $name,
- 'options_display' => array(),
- 'extension_slug' => $extension_slug,
+ 'name' => $name,
+ 'description' => $this->nullable_string_param( $request, 'description' ),
+ 'billing_policy' => BillingPolicy::from_array( $billing_policy ),
+ 'pricing_policy' => $this->pricing_policy_from_param( $request->get_param( 'pricing_policy' ), null ),
+ 'category' => $this->string_param( $request, 'category', Plan::DEFAULT_CATEGORY ),
+ 'status' => $this->string_param( $request, 'status', Plan::STATUS_ACTIVE ),
+ 'sort_order' => ScalarCoercion::coerce_int( $request->get_param( 'sort_order' ) ),
+ 'extension_slug' => $extension_slug,
)
);
- $group_id = $this->plan_group_repository->insert( $group );
-
- $plan = Plan::create(
- $group_id,
- $plan_args
- );
$this->plan_repository->insert( $plan );
} catch ( Throwable $e ) {
return $this->invalid_error( $e->getMessage() );
@@ -348,14 +324,12 @@ final class PlansController extends WP_REST_Controller {
}
try {
- $sync_group_name = null;
if ( $request->has_param( 'name' ) ) {
$name = $this->string_param( $request, 'name' );
if ( '' === $name ) {
return $this->invalid_error( __( 'Plan name is required.', 'woocommerce-subscriptions-engine' ) );
}
$plan->set_name( $name );
- $sync_group_name = $name;
}
if ( $request->has_param( 'description' ) ) {
@@ -389,10 +363,13 @@ final class PlansController extends WP_REST_Controller {
$plan->set_sort_order( ScalarCoercion::coerce_int( $request->get_param( 'sort_order' ) ) );
}
- if ( null !== $sync_group_name ) {
- $this->sync_group_name( $plan, $sync_group_name );
+ if ( ! $this->plan_repository->update( $plan ) ) {
+ return new WP_Error(
+ 'woocommerce_subscriptions_engine_plan_update_failed',
+ __( 'The plan could not be saved.', 'woocommerce-subscriptions-engine' ),
+ array( 'status' => 500 )
+ );
}
- $this->plan_repository->update( $plan );
} catch ( Throwable $e ) {
return $this->invalid_error( $e->getMessage() );
}
@@ -451,7 +428,6 @@ final class PlansController extends WP_REST_Controller {
*/
public function prepare_item_for_response( $item, $request ) {
$pricing = $item->get_pricing_policy();
- $group = $this->plan_group_repository->find( $item->get_group_id() );
$data = array(
'id' => $item->get_id(),
@@ -463,13 +439,6 @@ final class PlansController extends WP_REST_Controller {
'extension_slug' => $item->get_extension_slug(),
'billing_policy' => $item->get_billing_policy()->to_array(),
'pricing_policy' => null !== $pricing ? $pricing->to_array() : null,
- 'group' => $group instanceof PlanGroup
- ? array(
- 'id' => $group->get_id(),
- 'name' => $group->get_name(),
- 'options_display' => $group->get_options_display(),
- )
- : null,
);
$context = ScalarCoercion::coerce_string( $request->get_param( 'context' ), 'view' );
@@ -596,12 +565,6 @@ final class PlansController extends WP_REST_Controller {
'type' => array( 'object', 'null' ),
'context' => array( 'view', 'edit' ),
),
- 'group' => array(
- 'description' => __( 'Plan group.', 'woocommerce-subscriptions-engine' ),
- 'type' => array( 'object', 'null' ),
- 'context' => array( 'view' ),
- 'readonly' => true,
- ),
),
);
@@ -651,22 +614,6 @@ final class PlansController extends WP_REST_Controller {
return PricingPolicy::from_array( $data );
}
- /**
- * Sync a one-plan group's display name with the plan name.
- *
- * @param Plan $plan Plan.
- * @param string $name New name.
- */
- private function sync_group_name( Plan $plan, string $name ): void {
- $group = $this->plan_group_repository->find( $plan->get_group_id() );
- if ( ! $group instanceof PlanGroup ) {
- return;
- }
-
- $group->set_name( $name );
- $this->plan_group_repository->update( $group );
- }
-
/**
* Get multiple, valid extension slugs from an incoming request.
*
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 43d02403c09..282b8a0b645 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/Plan.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/Plan.php
@@ -45,13 +45,6 @@ final class Plan {
*/
private $id;
- /**
- * Parent plan group id.
- *
- * @var int
- */
- private $group_id;
-
/**
* Display name.
*
@@ -66,13 +59,6 @@ final class Plan {
*/
private $description;
- /**
- * Picker options, e.g. [{ name, value }].
- *
- * @var array<int, mixed>
- */
- private $options;
-
/**
* Billing cadence. Required - every plan has one.
*
@@ -115,6 +101,14 @@ final class Plan {
*/
private $sort_order;
+ /**
+ * Optional stable external identifier, unique at the storage layer - the
+ * consumer-side dedup key. Immutable post-create.
+ *
+ * @var string|null
+ */
+ private $merchant_code;
+
/**
* Owning extension slug, or null until owner semantics are assigned.
*
@@ -126,56 +120,52 @@ final class Plan {
* Use {@see self::create()} or {@see self::from_storage()}.
*
* @param int|null $id Plan id, or null before save.
- * @param int $group_id Parent plan group id.
* @param string $name Display name.
* @param string|null $description Optional description.
- * @param array<int, mixed> $options Picker options.
* @param BillingPolicy $billing_policy Billing cadence.
* @param DeliveryPolicy|null $delivery_policy Optional delivery policy.
* @param PricingPolicy|null $pricing_policy Optional pricing policy.
* @param string $category Plan category.
* @param string $status Merchant lifecycle status.
* @param int $sort_order Manual display order.
+ * @param string|null $merchant_code Optional stable external identifier.
* @param string|null $extension_slug Owning extension slug.
*/
private function __construct(
?int $id,
- int $group_id,
string $name,
?string $description,
- array $options,
BillingPolicy $billing_policy,
?DeliveryPolicy $delivery_policy,
?PricingPolicy $pricing_policy,
string $category,
string $status,
int $sort_order,
+ ?string $merchant_code,
?string $extension_slug
) {
self::validate_status( $status );
$this->id = $id;
- $this->group_id = $group_id;
$this->name = $name;
$this->description = $description;
- $this->options = $options;
$this->billing_policy = $billing_policy;
$this->delivery_policy = $delivery_policy;
$this->pricing_policy = $pricing_policy;
$this->category = $category;
$this->status = $status;
$this->sort_order = $sort_order;
+ $this->merchant_code = $merchant_code;
$this->extension_slug = $extension_slug;
}
/**
* Build a new, unsaved plan.
*
- * @param int $group_id Parent plan group id.
- * @param array<string, mixed> $args Plan attributes.
+ * @param array<string, mixed> $args Plan attributes.
* @throws InvalidArgumentException If pricing_policy entries fail validation.
*/
- public static function create( int $group_id, array $args ): self {
+ public static function create( array $args ): self {
$pricing_policy = $args['pricing_policy'] ?? null;
if ( null !== $pricing_policy && ! $pricing_policy instanceof PricingPolicy ) {
throw new InvalidArgumentException( 'Plan: pricing_policy must be a PricingPolicy instance or null.' );
@@ -196,16 +186,15 @@ final class Plan {
return new self(
null,
- $group_id,
ScalarCoercion::coerce_string( $args['name'] ?? null ),
ScalarCoercion::coerce_nullable_string( $args['description'] ?? null ),
- is_array( $args['options'] ?? null ) ? $args['options'] : array(),
$billing_policy,
$delivery_policy,
$pricing_policy,
ScalarCoercion::coerce_string( $args['category'] ?? null, self::DEFAULT_CATEGORY ),
ScalarCoercion::coerce_string( $args['status'] ?? null, self::DEFAULT_STATUS ),
ScalarCoercion::coerce_int( $args['sort_order'] ?? null, 0 ),
+ ScalarCoercion::coerce_nullable_string( $args['merchant_code'] ?? null ),
ScalarCoercion::coerce_nullable_string( $args['extension_slug'] ?? null )
);
}
@@ -231,16 +220,15 @@ final class Plan {
return new self(
isset( $row['id'] ) ? ScalarCoercion::coerce_int( $row['id'] ) : null,
- ScalarCoercion::coerce_int( $row['group_id'] ?? null ),
ScalarCoercion::coerce_string( $row['name'] ?? null ),
ScalarCoercion::coerce_nullable_string( $row['description'] ?? null ),
- is_array( $row['options'] ?? null ) ? $row['options'] : array(),
BillingPolicy::from_array( is_array( $row['billing_policy'] ?? null ) ? $row['billing_policy'] : array() ),
isset( $row['delivery_policy'] ) && is_array( $row['delivery_policy'] ) ? DeliveryPolicy::from_array( $row['delivery_policy'] ) : null,
$pricing_policy,
ScalarCoercion::coerce_string( $row['category'] ?? null, self::DEFAULT_CATEGORY ),
ScalarCoercion::coerce_string( $row['status'] ?? null, self::DEFAULT_STATUS ),
ScalarCoercion::coerce_int( $row['sort_order'] ?? null, 0 ),
+ ScalarCoercion::coerce_nullable_string( $row['merchant_code'] ?? null ),
ScalarCoercion::coerce_nullable_string( $row['extension_slug'] ?? null )
);
}
@@ -261,13 +249,6 @@ final class Plan {
$this->id = $id;
}
- /**
- * Parent plan group id.
- */
- public function get_group_id(): int {
- return $this->group_id;
- }
-
/**
* Display name.
*/
@@ -300,24 +281,6 @@ final class Plan {
$this->description = $description;
}
- /**
- * Picker options.
- *
- * @return array<int, mixed>
- */
- public function get_options(): array {
- return $this->options;
- }
-
- /**
- * Set the picker options.
- *
- * @param array<int, mixed> $options Picker options.
- */
- public function set_options( array $options ): void {
- $this->options = $options;
- }
-
/**
* Billing cadence.
*/
@@ -420,6 +383,13 @@ final class Plan {
$this->sort_order = $sort_order;
}
+ /**
+ * Optional stable external identifier, or null. Immutable post-create.
+ */
+ public function get_merchant_code(): ?string {
+ return $this->merchant_code;
+ }
+
/**
* Owning extension slug, or null.
*/
@@ -470,16 +440,15 @@ final class Plan {
*/
public function to_storage(): array {
return array(
- 'group_id' => $this->group_id,
'name' => $this->name,
'description' => $this->description,
- 'options' => $this->options,
'billing_policy' => $this->billing_policy->to_array(),
'delivery_policy' => null !== $this->delivery_policy ? $this->delivery_policy->to_array() : null,
'pricing_policy' => null !== $this->pricing_policy ? $this->pricing_policy->to_array() : null,
'category' => $this->category,
'status' => $this->status,
'sort_order' => $this->sort_order,
+ 'merchant_code' => $this->merchant_code,
'extension_slug' => $this->extension_slug,
);
}
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/PlanGroup.php b/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/PlanGroup.php
deleted file mode 100644
index 5dac0f094f8..00000000000
--- a/packages/php/woocommerce-subscriptions-engine/src/Core/Entity/PlanGroup.php
+++ /dev/null
@@ -1,187 +0,0 @@
-<?php
-/**
- * PlanGroup - a merchandising container for selling plans.
- *
- * `merchant_code` is an optional stable external identifier; when present it is
- * unique at the storage layer and is the deduplication key consumers use to
- * make group creation idempotent. `extension_slug` scopes a group to a specific extension.
- *
- * @package Automattic\WooCommerce\SubscriptionsEngine\Core\Entity
- */
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\SubscriptionsEngine\Core\Entity;
-
-use Automattic\WooCommerce\SubscriptionsEngine\Core\Support\ScalarCoercion;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * PlanGroup entity.
- *
- * Construct via {@see self::create()} for a new (unsaved) group or
- * {@see self::from_storage()} when hydrating a stored row.
- */
-final class PlanGroup {
-
- /**
- * Group id, or null before it is persisted.
- *
- * @var int|null
- */
- private $id;
-
- /**
- * Display name.
- *
- * @var string
- */
- private $name;
-
- /**
- * Optional stable external identifier; unique at the storage layer.
- *
- * @var string|null
- */
- private $merchant_code;
-
- /**
- * Display ordering metadata, e.g. [{ name, position }].
- *
- * @var array<int, mixed>
- */
- private $options_display;
-
- /**
- * Extension slug, e.g. a third-party extension slug.
- *
- * @var string|null
- */
- private $extension_slug;
-
- /**
- * Use {@see self::create()} or {@see self::from_storage()}.
- *
- * @param int|null $id Group id, or null before save.
- * @param string $name Display name.
- * @param string|null $merchant_code Optional stable external identifier.
- * @param array<int, mixed> $options_display Display ordering metadata.
- * @param string|null $extension_slug Extension slug.
- */
- private function __construct( ?int $id, string $name, ?string $merchant_code, array $options_display, ?string $extension_slug ) {
- $this->id = $id;
- $this->name = $name;
- $this->merchant_code = $merchant_code;
- $this->options_display = $options_display;
- $this->extension_slug = $extension_slug;
- }
-
- /**
- * Build a new, unsaved group.
- *
- * @param array<string, mixed> $args Group attributes.
- */
- public static function create( array $args ): self {
- return new self(
- null,
- ScalarCoercion::coerce_string( $args['name'] ?? null ),
- ScalarCoercion::coerce_nullable_string( $args['merchant_code'] ?? null ),
- is_array( $args['options_display'] ?? null ) ? $args['options_display'] : array(),
- ScalarCoercion::coerce_nullable_string( $args['extension_slug'] ?? null )
- );
- }
-
- /**
- * Hydrate from a stored row.
- *
- * @param array<string, mixed> $row Decoded plan-group row.
- */
- public static function from_storage( array $row ): self {
- return new self(
- isset( $row['id'] ) ? ScalarCoercion::coerce_int( $row['id'] ) : null,
- ScalarCoercion::coerce_string( $row['name'] ?? null ),
- ScalarCoercion::coerce_nullable_string( $row['merchant_code'] ?? null ),
- is_array( $row['options_display'] ?? null ) ? $row['options_display'] : array(),
- ScalarCoercion::coerce_nullable_string( $row['extension_slug'] ?? null )
- );
- }
-
- /**
- * Group id, or null before save.
- */
- public function get_id(): ?int {
- return $this->id;
- }
-
- /**
- * Assign the id after a successful insert.
- *
- * @param int $id Group id.
- */
- public function set_id( int $id ): void {
- $this->id = $id;
- }
-
- /**
- * Display name.
- */
- public function get_name(): string {
- return $this->name;
- }
-
- /**
- * Set the display name.
- *
- * @param string $name Display name.
- */
- public function set_name( string $name ): void {
- $this->name = $name;
- }
-
- /**
- * Optional stable external identifier; unique at the storage layer.
- */
- public function get_merchant_code(): ?string {
- return $this->merchant_code;
- }
-
- /**
- * Display ordering metadata.
- *
- * @return array<int, mixed>
- */
- public function get_options_display(): array {
- return $this->options_display;
- }
-
- /**
- * Set the display ordering metadata.
- *
- * @param array<int, mixed> $options_display Display ordering metadata.
- */
- public function set_options_display( array $options_display ): void {
- $this->options_display = $options_display;
- }
-
- /**
- * Extension slug.
- */
- public function get_extension_slug(): ?string {
- return $this->extension_slug;
- }
-
- /**
- * Serialize to the storage column shape (excluding generated id/timestamps).
- *
- * @return array<string, mixed>
- */
- public function to_storage(): array {
- return array(
- 'name' => $this->name,
- 'merchant_code' => $this->merchant_code,
- 'options_display' => $this->options_display,
- 'extension_slug' => $this->extension_slug,
- );
- }
-}
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Integration/Storage/PlanGroupRepository.php b/packages/php/woocommerce-subscriptions-engine/src/Integration/Storage/PlanGroupRepository.php
deleted file mode 100644
index 7d3b0c8a6f7..00000000000
--- a/packages/php/woocommerce-subscriptions-engine/src/Integration/Storage/PlanGroupRepository.php
+++ /dev/null
@@ -1,146 +0,0 @@
-<?php
-/**
- * PlanGroupRepository - persistence for {@see PlanGroup} entities.
- *
- * The engine's tables are private API; consumers reach plan groups through the public surface.
- *
- * @package Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage
- */
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage;
-
-use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\PlanGroup;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * PlanGroup repository.
- */
-final class PlanGroupRepository {
-
- /**
- * Insert a new plan group and stamp its id back onto the entity.
- *
- * @param PlanGroup $group Group to insert.
- * @return int The new group id.
- * @throws \RuntimeException If the insert fails.
- */
- public function insert( PlanGroup $group ): int {
- global $wpdb;
-
- $now = gmdate( 'Y-m-d H:i:s' );
- $data = $group->to_storage();
-
- // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
- $inserted = $wpdb->insert(
- SchemaInstaller::get_table_name( SchemaInstaller::TABLE_PLAN_GROUPS ),
- array(
- 'name' => $data['name'],
- 'merchant_code' => $data['merchant_code'],
- 'options_display' => wp_json_encode( $data['options_display'] ),
- 'extension_slug' => $data['extension_slug'],
- 'date_created_gmt' => $now,
- 'date_updated_gmt' => $now,
- )
- );
-
- if ( false === $inserted ) {
- throw new \RuntimeException( 'Failed to insert plan group.' );
- }
-
- $id = (int) $wpdb->insert_id;
- $group->set_id( $id );
-
- return $id;
- }
-
- /**
- * Fetch a plan group by id.
- *
- * @param int $id Group id.
- * @return PlanGroup|null Hydrated group, or null if not found.
- */
- public function find( int $id ): ?PlanGroup {
- global $wpdb;
-
- $table = SchemaInstaller::get_table_name( SchemaInstaller::TABLE_PLAN_GROUPS );
-
- // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
- $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ), ARRAY_A );
-
- if ( null === $row ) {
- return null;
- }
-
- $row['options_display'] = self::decode_json( $row['options_display'] );
-
- return PlanGroup::from_storage( $row );
- }
-
- /**
- * Persist changes to an existing plan group.
- *
- * @param PlanGroup $group Group to update. Must have an id.
- * @return bool True on success.
- * @throws \RuntimeException If the group has no id.
- */
- public function update( PlanGroup $group ): bool {
- global $wpdb;
-
- $id = $group->get_id();
- if ( null === $id ) {
- throw new \RuntimeException( 'Cannot update a plan group that has no id.' );
- }
-
- $data = $group->to_storage();
-
- // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
- $updated = $wpdb->update(
- SchemaInstaller::get_table_name( SchemaInstaller::TABLE_PLAN_GROUPS ),
- array(
- 'name' => $data['name'],
- 'options_display' => wp_json_encode( $data['options_display'] ),
- 'date_updated_gmt' => gmdate( 'Y-m-d H:i:s' ),
- ),
- array( 'id' => $id )
- );
-
- return false !== $updated;
- }
-
- /**
- * Delete a plan group by id.
- *
- * @param int $id Group id.
- * @return bool True when a row was removed.
- */
- public function delete( int $id ): bool {
- global $wpdb;
-
- // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
- $deleted = $wpdb->delete(
- SchemaInstaller::get_table_name( SchemaInstaller::TABLE_PLAN_GROUPS ),
- array( 'id' => $id )
- );
-
- return (bool) $deleted;
- }
-
- /**
- * Decode a JSON column into an array, tolerating null/empty values.
- *
- * @param mixed $value Raw column value.
- * @return array<mixed>
- */
- private static function decode_json( $value ): array {
- if ( ! is_string( $value ) || '' === $value ) {
- return array();
- }
-
- $decoded = json_decode( $value, true );
-
- return is_array( $decoded ) ? $decoded : array();
- }
-}
diff --git a/packages/php/woocommerce-subscriptions-engine/src/Integration/Storage/PlanRepository.php b/packages/php/woocommerce-subscriptions-engine/src/Integration/Storage/PlanRepository.php
index 68961444f34..bb5bd1c11de 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Integration/Storage/PlanRepository.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Integration/Storage/PlanRepository.php
@@ -24,7 +24,7 @@ final class PlanRepository {
*
* @var array<int, string>
*/
- private const JSON_COLUMNS = array( 'options', 'billing_policy', 'delivery_policy', 'pricing_policy' );
+ private const JSON_COLUMNS = array( 'billing_policy', 'delivery_policy', 'pricing_policy' );
/**
* Always-false WHERE clause: a filter arg that is present but empty or
@@ -53,9 +53,13 @@ final class PlanRepository {
/**
* Insert a new plan and stamp its id back onto the entity.
*
+ * `merchant_code` uniqueness is DB-enforced per extension (composite UNIQUE
+ * with `extension_slug`, NULLs distinct): a duplicate code within one
+ * extension fails the insert and surfaces as the RuntimeException.
+ *
* @param Plan $plan Plan to insert.
* @return int The new plan id.
- * @throws \RuntimeException If the insert fails.
+ * @throws \RuntimeException If the insert fails, including on a duplicate merchant_code.
*/
public function insert( Plan $plan ): int {
global $wpdb;
@@ -67,10 +71,8 @@ final class PlanRepository {
$inserted = $wpdb->insert(
SchemaInstaller::get_table_name( SchemaInstaller::TABLE_PLANS ),
array(
- 'group_id' => $data['group_id'],
'name' => $data['name'],
'description' => $data['description'],
- 'options' => wp_json_encode( $data['options'] ),
'billing_policy' => wp_json_encode( $data['billing_policy'] ),
'delivery_policy' => null !== $data['delivery_policy'] ? wp_json_encode( $data['delivery_policy'] ) : null,
'inventory_policy' => null,
@@ -78,6 +80,7 @@ final class PlanRepository {
'category' => $data['category'],
'status' => $data['status'],
'sort_order' => $data['sort_order'],
+ 'merchant_code' => $data['merchant_code'],
'extension_slug' => $data['extension_slug'],
'date_created_gmt' => $now,
'date_updated_gmt' => $now,
@@ -208,6 +211,9 @@ final class PlanRepository {
/**
* Persist changes to an existing plan.
*
+ * `merchant_code` is immutable post-create and intentionally not written here,
+ * same as `id`.
+ *
* @param Plan $plan Plan to update. Must have an id.
* @return bool True on success.
* @throws \RuntimeException If the plan has no id.
@@ -228,7 +234,6 @@ final class PlanRepository {
array(
'name' => $data['name'],
'description' => $data['description'],
- 'options' => wp_json_encode( $data['options'] ),
'billing_policy' => wp_json_encode( $data['billing_policy'] ),
'delivery_policy' => null !== $data['delivery_policy'] ? wp_json_encode( $data['delivery_policy'] ) : null,
'pricing_policy' => null !== $data['pricing_policy'] ? wp_json_encode( $data['pricing_policy'] ) : null,
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 036096ab2b0..76b73801b06 100644
--- a/packages/php/woocommerce-subscriptions-engine/src/Integration/Storage/SchemaInstaller.php
+++ b/packages/php/woocommerce-subscriptions-engine/src/Integration/Storage/SchemaInstaller.php
@@ -37,13 +37,15 @@ final class SchemaInstaller {
* 2.2.0 - dispatcher columns: cycle `claimed_until` (crash-recovery lease) and
* reserved `retry_at`; a `due_contract (status, next_payment_gmt)` index on
* contracts for the batch renewal scan.
+ * 2.3.0 - catalog flatten: drop the plan_groups table; plans lose group_id and
+ * options, gain merchant_code (UNIQUE).
*
* Pre-freeze, tables are recreated rather than migrated. dbDelta adds columns but
* does not change an existing column's nullability or drop unused ones, so a dev box
* 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.
*/
- private const VERSION = '2.2.0';
+ private const VERSION = '2.3.0';
/**
* Option key tracking the installed schema version.
@@ -53,7 +55,6 @@ final class SchemaInstaller {
/**
* Logical table identifiers - keys map to unprefixed table names.
*/
- public const TABLE_PLAN_GROUPS = 'plan_groups';
public const TABLE_PLANS = 'plans';
public const TABLE_CONTRACTS = 'contracts';
public const TABLE_CONTRACT_ITEMS = 'contract_items';
@@ -162,7 +163,6 @@ final class SchemaInstaller {
*/
private static function get_table_names( string $prefix ): array {
return array(
- self::TABLE_PLAN_GROUPS => $prefix . 'wc_selling_plan_groups',
self::TABLE_PLANS => $prefix . 'wc_selling_plans',
self::TABLE_CONTRACTS => $prefix . 'wc_subscription_contracts',
self::TABLE_CONTRACT_ITEMS => $prefix . 'wc_subscription_contract_items',
@@ -185,7 +185,6 @@ final class SchemaInstaller {
* @return array<int, string>
*/
private static function get_table_definitions( array $names, string $collate ): array {
- $plan_groups = $names[ self::TABLE_PLAN_GROUPS ];
$plans = $names[ self::TABLE_PLANS ];
$contracts = $names[ self::TABLE_CONTRACTS ];
$contract_items = $names[ self::TABLE_CONTRACT_ITEMS ];
@@ -194,31 +193,16 @@ final class SchemaInstaller {
$cycles = $names[ self::TABLE_CYCLES ];
$snapshots = $names[ self::TABLE_SNAPSHOTS ];
- // `merchant_code` is UNIQUE (not KEY) for DB-enforced idempotency on
- // consumer-supplied codes; NULLs are treated as distinct, so consumers that do
- // not use merchant codes are unaffected.
- $plan_groups_sql = "CREATE TABLE {$plan_groups} (
- id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
- name VARCHAR(255) NOT NULL,
- merchant_code VARCHAR(64) NULL,
- options_display JSON NULL,
- extension_slug VARCHAR(64) NULL,
- date_created_gmt DATETIME NOT NULL,
- date_updated_gmt DATETIME NOT NULL,
- PRIMARY KEY (id),
- UNIQUE KEY merchant_code (merchant_code),
- KEY extension_slug (extension_slug)
-) {$collate};";
-
- // `extension_slug` records the creating extension's registered slug. Nullable
- // while owner identifier/registration semantics are still open; tightened
- // additively once decided.
+ // `merchant_code` is DB-enforced-unique per extension (composite with
+ // `extension_slug`) for idempotency on consumer-supplied codes - each consumer
+ // owns its own code namespace; NULLs are treated as distinct, so consumers that
+ // do not use merchant codes are unaffected. `extension_slug` records the creating
+ // extension's registered slug. Nullable while owner identifier/registration
+ // semantics are still open; tightened additively once decided.
$plans_sql = "CREATE TABLE {$plans} (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
- group_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT NULL,
- options JSON NOT NULL,
billing_policy JSON NOT NULL,
delivery_policy JSON NULL,
inventory_policy JSON NULL,
@@ -226,11 +210,12 @@ final class SchemaInstaller {
category VARCHAR(32) NOT NULL DEFAULT 'SUBSCRIPTION',
status VARCHAR(20) NOT NULL DEFAULT 'active',
sort_order INT NOT NULL DEFAULT 0,
+ merchant_code VARCHAR(64) NULL,
extension_slug VARCHAR(64) NULL,
date_created_gmt DATETIME NOT NULL,
date_updated_gmt DATETIME NOT NULL,
PRIMARY KEY (id),
- KEY group_id (group_id),
+ UNIQUE KEY extension_merchant_code (extension_slug, merchant_code),
KEY category (category),
KEY status_sort (status, sort_order, id),
KEY extension_slug (extension_slug)
@@ -381,7 +366,6 @@ final class SchemaInstaller {
) {$collate};";
return array(
- $plan_groups_sql,
$plans_sql,
$contracts_sql,
$contract_items_sql,
diff --git a/packages/php/woocommerce-subscriptions-engine/tests/integration/Api/Rest/PlansControllerTest.php b/packages/php/woocommerce-subscriptions-engine/tests/integration/Api/Rest/PlansControllerTest.php
index f51b60f5bcb..33d490837ae 100644
--- a/packages/php/woocommerce-subscriptions-engine/tests/integration/Api/Rest/PlansControllerTest.php
+++ b/packages/php/woocommerce-subscriptions-engine/tests/integration/Api/Rest/PlansControllerTest.php
@@ -96,6 +96,8 @@ class PlansControllerTest extends EngineIntegrationTestCase {
$this->assertSame( 'global', $created_data['scope'] );
$this->assertSame( Plan::STATUS_ACTIVE, $created_data['status'] );
$this->assertSame( self::EXTENSION_SLUG, $created_data['extension_slug'] );
+ $this->assertArrayNotHasKey( 'group', $created_data );
+ $this->assertArrayNotHasKey( 'merchant_code', $created_data );
$id = $this->int_value( $created_data, 'id' );
@@ -317,6 +319,42 @@ class PlansControllerTest extends EngineIntegrationTestCase {
}
}
+ public function test_update_surfaces_a_failed_write_as_an_error(): void {
+ global $wpdb;
+
+ wp_set_current_user( $this->admin_id );
+ $id = $this->create_plan( 'Doomed to fail' );
+
+ // Break UPDATEs against the plans table for this request so the write
+ // errors at the database and the repository reports failure.
+ $break_plan_updates = static function ( $query ) {
+ if ( is_string( $query ) && 0 === stripos( ltrim( $query ), 'UPDATE' ) && false !== strpos( $query, 'wc_selling_plans' ) ) {
+ return 'UPDATE nonexistent_table_for_this_test SET id = id';
+ }
+
+ return $query;
+ };
+ add_filter( 'query', $break_plan_updates );
+ $suppressed = $wpdb->suppress_errors( true );
+
+ try {
+ $response = $this->request(
+ 'PATCH',
+ self::BASE . '/' . $id,
+ array(
+ 'extension_slug' => self::EXTENSION_SLUG,
+ 'name' => 'New name',
+ )
+ );
+ } finally {
+ $wpdb->suppress_errors( $suppressed );
+ remove_filter( 'query', $break_plan_updates );
+ }
+
+ $this->assertSame( 500, $response->get_status() );
+ $this->assertSame( 'woocommerce_subscriptions_engine_plan_update_failed', $this->response_data( $response )['code'] );
+ }
+
public function test_archive_restore_and_reorder(): void {
wp_set_current_user( $this->admin_id );
diff --git a/packages/php/woocommerce-subscriptions-engine/tests/integration/Api/SellingPlansTest.php b/packages/php/woocommerce-subscriptions-engine/tests/integration/Api/SellingPlansTest.php
index 6635a8f998a..da400c7c9a8 100644
--- a/packages/php/woocommerce-subscriptions-engine/tests/integration/Api/SellingPlansTest.php
+++ b/packages/php/woocommerce-subscriptions-engine/tests/integration/Api/SellingPlansTest.php
@@ -12,9 +12,7 @@ namespace Automattic\WooCommerce\SubscriptionsEngine\Tests\Integration\Api;
use EngineIntegrationTestCase;
use Automattic\WooCommerce\SubscriptionsEngine\Api\SellingPlans;
use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\Plan;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\PlanGroup;
use Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\BillingPolicy;
-use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanGroupRepository;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanRepository;
/**
@@ -24,35 +22,15 @@ class SellingPlansTest extends EngineIntegrationTestCase {
private const SLUG = 'lite';
- /**
- * Insert a plan group.
- *
- * @param string $merchant_code Merchant code.
- * @param string $extension_slug Owning extension slug.
- */
- private function make_group( string $merchant_code, string $extension_slug = self::SLUG ): int {
- return ( new PlanGroupRepository() )->insert(
- PlanGroup::create(
- array(
- 'name' => 'Group ' . $merchant_code,
- 'merchant_code' => $merchant_code,
- 'extension_slug' => $extension_slug,
- )
- )
- );
- }
-
/**
* Insert a plan.
*
- * @param int $group_id Parent group id.
* @param string $name Plan name.
* @param array<string, mixed> $overrides Attribute overrides.
*/
- private function make_plan( int $group_id, string $name, array $overrides = array() ): int {
+ private function make_plan( string $name, array $overrides = array() ): int {
return ( new PlanRepository() )->insert(
Plan::create(
- $group_id,
array_merge(
array(
'name' => $name,
@@ -86,12 +64,10 @@ class SellingPlansTest extends EngineIntegrationTestCase {
}
public function test_list_plans_excludes_archived_and_foreign_slug_plans(): void {
- $group_id = $this->make_group( 'listing' );
-
- $second_id = $this->make_plan( $group_id, 'Second', array( 'sort_order' => 2 ) );
- $first_id = $this->make_plan( $group_id, 'First', array( 'sort_order' => 1 ) );
- $this->make_plan( $group_id, 'Archived', array( 'status' => Plan::STATUS_ARCHIVED ) );
- $this->make_plan( $group_id, 'Foreign', array( 'extension_slug' => 'other-extension' ) );
+ $second_id = $this->make_plan( 'Second', array( 'sort_order' => 2 ) );
+ $first_id = $this->make_plan( 'First', array( 'sort_order' => 1 ) );
+ $this->make_plan( 'Archived', array( 'status' => Plan::STATUS_ARCHIVED ) );
+ $this->make_plan( 'Foreign', array( 'extension_slug' => 'other-extension' ) );
$plans = ( new SellingPlans( array( self::SLUG ) ) )->list_plans();
@@ -99,13 +75,11 @@ class SellingPlansTest extends EngineIntegrationTestCase {
}
public function test_get_plans_returns_active_owned_plans_in_display_order(): void {
- $group_id = $this->make_group( 'fetching' );
-
- $second_id = $this->make_plan( $group_id, 'Second', array( 'sort_order' => 2 ) );
- $first_id = $this->make_plan( $group_id, 'First', array( 'sort_order' => 1 ) );
- $excluded_id = $this->make_plan( $group_id, 'Excluded', array( 'sort_order' => 3 ) );
- $archived_id = $this->make_plan( $group_id, 'Archived', array( 'status' => Plan::STATUS_ARCHIVED ) );
- $foreign_id = $this->make_plan( $group_id, 'Foreign', array( 'extension_slug' => 'other-extension' ) );
+ $second_id = $this->make_plan( 'Second', array( 'sort_order' => 2 ) );
+ $first_id = $this->make_plan( 'First', array( 'sort_order' => 1 ) );
+ $excluded_id = $this->make_plan( 'Excluded', array( 'sort_order' => 3 ) );
+ $archived_id = $this->make_plan( 'Archived', array( 'status' => Plan::STATUS_ARCHIVED ) );
+ $foreign_id = $this->make_plan( 'Foreign', array( 'extension_slug' => 'other-extension' ) );
$catalog = new SellingPlans( array( self::SLUG ) );
@@ -117,8 +91,7 @@ class SellingPlansTest extends EngineIntegrationTestCase {
}
public function test_get_plans_empty_or_invalid_ids_yield_an_empty_array(): void {
- $group_id = $this->make_group( 'empty-ids' );
- $plan_id = $this->make_plan( $group_id, 'Plan' );
+ $plan_id = $this->make_plan( 'Plan' );
$catalog = new SellingPlans( array( self::SLUG ) );
@@ -128,12 +101,8 @@ class SellingPlansTest extends EngineIntegrationTestCase {
}
public function test_two_slug_instance_reads_across_both_slugs(): void {
- $lite_group_id = $this->make_group( 'two-slug-lite' );
- $other_group_id = $this->make_group( 'two-slug-other', 'other-extension' );
-
- $lite_id = $this->make_plan( $lite_group_id, 'Lite plan', array( 'sort_order' => 1 ) );
+ $lite_id = $this->make_plan( 'Lite plan', array( 'sort_order' => 1 ) );
$other_id = $this->make_plan(
- $other_group_id,
'Other plan',
array(
'sort_order' => 2,
@@ -141,7 +110,6 @@ class SellingPlansTest extends EngineIntegrationTestCase {
)
);
$foreign_id = $this->make_plan(
- $lite_group_id,
'Foreign',
array(
'sort_order' => 3,
diff --git a/packages/php/woocommerce-subscriptions-engine/tests/integration/Api/SubscriptionsTest.php b/packages/php/woocommerce-subscriptions-engine/tests/integration/Api/SubscriptionsTest.php
index 0284a6d6fc4..fd19a170632 100644
--- a/packages/php/woocommerce-subscriptions-engine/tests/integration/Api/SubscriptionsTest.php
+++ b/packages/php/woocommerce-subscriptions-engine/tests/integration/Api/SubscriptionsTest.php
@@ -17,14 +17,12 @@ 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\Entity\Plan;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\PlanGroup;
use Automattic\WooCommerce\SubscriptionsEngine\Core\Gateway\GatewayCapabilities;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Checkout\OrderLinkage;
use Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\BillingPolicy;
use Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\PlanSnapshot;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Checkout\ContractFactory;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\ContractRepository;
-use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanGroupRepository;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanRepository;
/**
@@ -57,9 +55,7 @@ class SubscriptionsTest extends EngineIntegrationTestCase {
* @return Contract The persisted contract with cycle 1 billed.
*/
private function sign_up_contract( int $customer_id = 0 ): Contract {
- $group_id = ( new PlanGroupRepository() )->insert( PlanGroup::create( array( 'name' => 'Club' ) ) );
- $plan = Plan::create(
- $group_id,
+ $plan = Plan::create(
array(
'name' => 'Monthly',
'billing_policy' => new BillingPolicy( 'month', 1, null, null, null ),
diff --git a/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Checkout/ContractFactoryTest.php b/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Checkout/ContractFactoryTest.php
index a363b35c0da..47e6f6f2452 100644
--- a/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Checkout/ContractFactoryTest.php
+++ b/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Checkout/ContractFactoryTest.php
@@ -16,12 +16,10 @@ 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\Entity\Plan;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\PlanGroup;
use Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\BillingPolicy;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Checkout\ContractFactory;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Checkout\OrderLinkage;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\ContractRepository;
-use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanGroupRepository;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanRepository;
/**
@@ -35,12 +33,7 @@ class ContractFactoryTest extends EngineIntegrationTestCase {
* @param array{length: int, unit: string}|null $trial Native trial duration, or null for none.
*/
private function make_plan( ?int $max_cycles = null, ?array $trial = null ): Plan {
- $group_id = ( new PlanGroupRepository() )->insert(
- PlanGroup::create( array( 'name' => 'Coffee club' ) )
- );
-
$plan = Plan::create(
- $group_id,
array(
'name' => 'Monthly coffee',
'billing_policy' => new BillingPolicy( 'month', 1, null, $max_cycles, $trial ),
@@ -241,7 +234,6 @@ class ContractFactoryTest extends EngineIntegrationTestCase {
public function test_unsaved_plan_is_rejected(): void {
$order = $this->make_order();
$plan = Plan::create(
- 1,
array(
'name' => 'Monthly coffee',
'billing_policy' => new BillingPolicy( 'month', 1, null, null, null ),
diff --git a/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Contracts/ReactivationTest.php b/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Contracts/ReactivationTest.php
index 9bc5d4bc861..ecc0452b01e 100644
--- a/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Contracts/ReactivationTest.php
+++ b/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Contracts/ReactivationTest.php
@@ -18,12 +18,10 @@ use EngineIntegrationTestCase;
use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\Contract;
use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\ContractStatus;
use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\Plan;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\PlanGroup;
use Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\BillingPolicy;
use Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\PlanSnapshot;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Contracts\Reactivation;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\ContractRepository;
-use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanGroupRepository;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanRepository;
/**
@@ -68,9 +66,7 @@ class ReactivationTest extends EngineIntegrationTestCase {
* @param string $period Billing period slug: day/week/month/year.
*/
private function make_plan( string $period ): int {
- $group_id = ( new PlanGroupRepository() )->insert( PlanGroup::create( array( 'name' => 'Club' ) ) );
- $plan = Plan::create(
- $group_id,
+ $plan = Plan::create(
array(
'name' => ucfirst( $period ) . 'ly',
'billing_policy' => new BillingPolicy( $period, 1, null, null, null ),
diff --git a/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Renewal/RenewalDispatcherTest.php b/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Renewal/RenewalDispatcherTest.php
index 9a16e282de1..85f3139de7f 100644
--- a/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Renewal/RenewalDispatcherTest.php
+++ b/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Renewal/RenewalDispatcherTest.php
@@ -18,7 +18,6 @@ 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\Entity\Plan;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\PlanGroup;
use Automattic\WooCommerce\SubscriptionsEngine\Core\Gateway\GatewayCapabilities;
use Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\BillingPolicy;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Checkout\ContractFactory;
@@ -26,7 +25,6 @@ use Automattic\WooCommerce\SubscriptionsEngine\Integration\Checkout\OrderLinkage
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Ownership\ConsumerRegistry;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Renewal\RenewalDispatcher;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\ContractRepository;
-use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanGroupRepository;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanRepository;
/**
@@ -69,10 +67,7 @@ class RenewalDispatcherTest extends EngineIntegrationTestCase {
* Persist a monthly plan and return the entity (the ContractFactory needs the plan).
*/
private function make_plan_object(): Plan {
- $group_id = ( new PlanGroupRepository() )->insert( PlanGroup::create( array( 'name' => 'Club' ) ) );
-
$plan = Plan::create(
- $group_id,
array(
'name' => 'Monthly',
'billing_policy' => new BillingPolicy( 'month', 1, null, null, null ),
diff --git a/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Renewal/RenewalEngineTest.php b/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Renewal/RenewalEngineTest.php
index 7cb95c4d2b7..1ce32294905 100644
--- a/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Renewal/RenewalEngineTest.php
+++ b/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Renewal/RenewalEngineTest.php
@@ -16,7 +16,6 @@ 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\Entity\Plan;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\PlanGroup;
use Automattic\WooCommerce\SubscriptionsEngine\Core\Gateway\GatewayCapabilities;
use Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\BillingPolicy;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Checkout\ContractFactory;
@@ -27,7 +26,6 @@ use Automattic\WooCommerce\SubscriptionsEngine\Integration\Renewal\RenewalDispat
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Renewal\RenewalEngine;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Renewal\RenewalIntent;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\ContractRepository;
-use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanGroupRepository;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanRepository;
/**
@@ -116,10 +114,7 @@ class RenewalEngineTest extends EngineIntegrationTestCase {
* @param int|null $max_cycles Maximum billing cycles, or null for open-ended.
*/
private function make_plan_object( ?int $max_cycles = null ): Plan {
- $group_id = ( new PlanGroupRepository() )->insert( PlanGroup::create( array( 'name' => 'Club' ) ) );
-
$plan = Plan::create(
- $group_id,
array(
'name' => 'Monthly',
'billing_policy' => new BillingPolicy( 'month', 1, null, $max_cycles, null ),
diff --git a/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Storage/PlanRepositoryTest.php b/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Storage/PlanRepositoryTest.php
index 977af3a7cf3..c66425483ff 100644
--- a/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Storage/PlanRepositoryTest.php
+++ b/packages/php/woocommerce-subscriptions-engine/tests/integration/Integration/Storage/PlanRepositoryTest.php
@@ -1,6 +1,6 @@
<?php
/**
- * Integration tests for PlanRepository (and PlanGroupRepository).
+ * Integration tests for PlanRepository.
*
* @package Automattic\WooCommerce\SubscriptionsEngine
*/
@@ -11,33 +11,18 @@ namespace Automattic\WooCommerce\SubscriptionsEngine\Tests\Integration\Integrati
use EngineIntegrationTestCase;
use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\Plan;
-use Automattic\WooCommerce\SubscriptionsEngine\Core\Entity\PlanGroup;
use Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\BillingPolicy;
use Automattic\WooCommerce\SubscriptionsEngine\Core\ValueObject\PricingPolicy;
-use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanGroupRepository;
use Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanRepository;
/**
* @covers \Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanRepository
- * @covers \Automattic\WooCommerce\SubscriptionsEngine\Integration\Storage\PlanGroupRepository
*/
class PlanRepositoryTest extends EngineIntegrationTestCase {
- private function make_group(): int {
- $group = PlanGroup::create(
- array(
- 'name' => 'Coffee club',
- 'merchant_code' => 'coffee-club',
- )
- );
-
- return ( new PlanGroupRepository() )->insert( $group );
- }
-
- private function make_plan( PlanRepository $repo, int $group_id, string $name, string $extension_slug, int $sort_order = 0 ): int {
+ private function make_plan( PlanRepository $repo, string $name, string $extension_slug, int $sort_order = 0 ): int {
return $repo->insert(
Plan::create(
- $group_id,
array(
'name' => $name,
'billing_policy' => BillingPolicy::from_array(
@@ -53,45 +38,13 @@ class PlanRepositoryTest extends EngineIntegrationTestCase {
);
}
- public function test_plan_group_round_trips(): void {
- $repo = new PlanGroupRepository();
-
- $id = $repo->insert(
- PlanGroup::create(
- array(
- 'name' => 'Boxes',
- 'merchant_code' => 'boxes',
- 'options_display' => array( array( 'name' => 'Size' ) ),
- 'extension_slug' => 'wc-subscriptions',
- )
- )
- );
-
- $fetched = $repo->find( $id );
-
- $this->assertInstanceOf( PlanGroup::class, $fetched );
- $this->assertSame( $id, $fetched->get_id() );
- $this->assertSame( 'Boxes', $fetched->get_name() );
- $this->assertSame( 'boxes', $fetched->get_merchant_code() );
- $this->assertSame( 'wc-subscriptions', $fetched->get_extension_slug() );
- $this->assertSame( array( array( 'name' => 'Size' ) ), $fetched->get_options_display() );
- }
-
public function test_plan_round_trips_with_policies_and_extension_slug(): void {
- $group_id = $this->make_group();
- $repo = new PlanRepository();
+ $repo = new PlanRepository();
$plan = Plan::create(
- $group_id,
array(
'name' => 'Monthly',
'description' => 'A monthly plan',
- 'options' => array(
- array(
- 'name' => 'Monthly',
- 'value' => 'monthly',
- ),
- ),
'billing_policy' => BillingPolicy::from_array(
array(
'period' => 'month',
@@ -124,7 +77,6 @@ class PlanRepositoryTest extends EngineIntegrationTestCase {
$this->assertInstanceOf( Plan::class, $fetched );
$this->assertSame( 'Monthly', $fetched->get_name() );
$this->assertSame( 'A monthly plan', $fetched->get_description() );
- $this->assertSame( $group_id, $fetched->get_group_id() );
$this->assertSame( 'lite', $fetched->get_extension_slug() );
$this->assertSame( Plan::STATUS_ARCHIVED, $fetched->get_status() );
$this->assertSame( 4, $fetched->get_sort_order() );
@@ -135,12 +87,10 @@ class PlanRepositoryTest extends EngineIntegrationTestCase {
}
public function test_plan_without_optional_policies_round_trips(): void {
- $group_id = $this->make_group();
- $repo = new PlanRepository();
+ $repo = new PlanRepository();
$id = $repo->insert(
Plan::create(
- $group_id,
array(
'name' => 'Bare',
'billing_policy' => BillingPolicy::from_array(
@@ -161,12 +111,99 @@ class PlanRepositoryTest extends EngineIntegrationTestCase {
$this->assertNull( $fetched->get_extension_slug() );
}
+ public function test_merchant_code_round_trips_through_insert_and_find(): void {
+ $repo = new PlanRepository();
+
+ $id = $repo->insert(
+ Plan::create(
+ array(
+ 'name' => 'Coded',
+ 'billing_policy' => BillingPolicy::from_array(
+ array(
+ 'period' => 'month',
+ 'interval' => 1,
+ )
+ ),
+ 'merchant_code' => 'coffee-club',
+ )
+ )
+ );
+
+ $fetched = $repo->find( $id );
+
+ $this->assertInstanceOf( Plan::class, $fetched );
+ $this->assertSame( 'coffee-club', $fetched->get_merchant_code() );
+ }
+
+ public function test_duplicate_merchant_code_insert_throws_within_one_extension(): void {
+ $repo = new PlanRepository();
+
+ $make = static function ( string $extension_slug ): Plan {
+ return Plan::create(
+ array(
+ 'name' => 'Duplicate code',
+ 'billing_policy' => BillingPolicy::from_array(
+ array(
+ 'period' => 'month',
+ 'interval' => 1,
+ )
+ ),
+ 'merchant_code' => 'dupe-code',
+ 'extension_slug' => $extension_slug,
+ )
+ );
+ };
+
+ $repo->insert( $make( 'lite' ) );
+
+ $this->expectException( \RuntimeException::class );
+ $repo->insert( $make( 'lite' ) );
+ }
+
+ public function test_same_merchant_code_coexists_across_extensions(): void {
+ $repo = new PlanRepository();
+
+ $make = static function ( string $extension_slug ): Plan {
+ return Plan::create(
+ array(
+ 'name' => 'Shared code',
+ 'billing_policy' => BillingPolicy::from_array(
+ array(
+ 'period' => 'month',
+ 'interval' => 1,
+ )
+ ),
+ 'merchant_code' => 'monthly-box',
+ 'extension_slug' => $extension_slug,
+ )
+ );
+ };
+
+ $first_id = $repo->insert( $make( 'lite' ) );
+ $second_id = $repo->insert( $make( 'other-extension' ) );
+
+ $this->assertGreaterThan( 0, $first_id );
+ $this->assertGreaterThan( $first_id, $second_id );
+ }
+
+ public function test_plans_without_merchant_code_coexist(): void {
+ $repo = new PlanRepository();
+
+ $first_id = $this->make_plan( $repo, 'First uncoded', 'lite' );
+ $second_id = $this->make_plan( $repo, 'Second uncoded', 'lite' );
+
+ $this->assertGreaterThan( 0, $first_id );
+ $this->assertGreaterThan( $first_id, $second_id );
+
+ $first = $repo->find( $first_id );
+ $this->assertInstanceOf( Plan::class, $first );
+ $this->assertNull( $first->get_merchant_code() );
+ }
+
public function test_update_persists_changes(): void {
- $group_id = $this->make_group();
- $repo = new PlanRepository();
+ $repo = new PlanRepository();
$plan = Plan::create(
- $group_id,
array(
'name' => 'Before',
'billing_policy' => BillingPolicy::from_array(
@@ -192,11 +229,9 @@ class PlanRepositoryTest extends EngineIntegrationTestCase {
}
public function test_query_count_and_reorder_use_plan_lifecycle_fields(): void {
- $group_id = $this->make_group();
- $repo = new PlanRepository();
+ $repo = new PlanRepository();
$first = Plan::create(
- $group_id,
array(
'name' => 'Alpha monthly',
'billing_policy' => BillingPolicy::from_array(
@@ -211,7 +246,6 @@ class PlanRepositoryTest extends EngineIntegrationTestCase {
)
);
$second = Plan::create(
- $group_id,
array(
'name' => 'Beta weekly',
'billing_policy' => BillingPolicy::from_array(
@@ -226,7 +260,6 @@ class PlanRepositoryTest extends EngineIntegrationTestCase {
)
);
$archived = Plan::create(
- $group_id,
array(
'name' => 'Archived yearly',
'billing_policy' => BillingPolicy::from_array(
@@ -299,11 +332,10 @@ class PlanRepositoryTest extends EngineIntegrationTestCase {
* @param string $search Search term.
*/
public function test_query_search_terms_starting_with_prepare_specifiers( string $search ): void {
- $group_id = $this->make_group();
- $repo = new PlanRepository();
+ $repo = new PlanRepository();
- $this->make_plan( $repo, $group_id, 'Unrelated prepare regression plan', 'lite' );
- $expected_id = $this->make_plan( $repo, $group_id, $search . ' plan', 'lite' );
+ $this->make_plan( $repo, 'Unrelated prepare regression plan', 'lite' );
+ $expected_id = $this->make_plan( $repo, $search . ' plan', 'lite' );
$query_args = array(
'extension_slugs' => array( 'lite' ),
@@ -323,10 +355,9 @@ class PlanRepositoryTest extends EngineIntegrationTestCase {
}
public function test_invalid_extension_scopes_do_not_return_unscoped_results(): void {
- $group_id = $this->make_group();
- $repo = new PlanRepository();
+ $repo = new PlanRepository();
- $id = $this->make_plan( $repo, $group_id, 'Scoped', 'lite' );
+ $id = $this->make_plan( $repo, 'Scoped', 'lite' );
$this->assertInstanceOf( Plan::class, $repo->find( $id, 'any' ) );
// Test with extension_slugs array.
@@ -347,11 +378,10 @@ class PlanRepositoryTest extends EngineIntegrationTestCase {
}
public function test_query_extension_slugs_filters_by_single_and_multiple_slugs(): void {
- $group_id = $this->make_group();
- $repo = new PlanRepository();
+ $repo = new PlanRepository();
- $lite_id = $this->make_plan( $repo, $group_id, 'Lite plan', 'lite', 1 );
- $other_id = $this->make_plan( $repo, $group_id, 'Other plan', 'other-extension', 2 );
+ $lite_id = $this->make_plan( $repo, 'Lite plan', 'lite', 1 );
+ $other_id = $this->make_plan( $repo, 'Other plan', 'other-extension', 2 );
$single = $repo->query( array( 'extension_slugs' => array( 'lite' ) ) );
$this->assertSame( array( $lite_id ), array_map( static fn ( Plan $plan ): ?int => $plan->get_id(), $single ) );
@@ -362,10 +392,9 @@ class PlanRepositoryTest extends EngineIntegrationTestCase {
}
public function test_query_singular_extension_slug_arg_is_unknown_and_ignored(): void {
- $group_id = $this->make_group();
- $repo = new PlanRepository();
+ $repo = new PlanRepository();
- $plan_id = $this->make_plan( $repo, $group_id, 'Scoped', 'lite' );
+ $plan_id = $this->make_plan( $repo, 'Scoped', 'lite' );
$plans = $repo->query( array( 'extension_slug' => 'other-extension' ) );
$this->assertSame( array( $plan_id ), array_map( static fn ( Plan $plan ): ?int => $plan->get_id(), $plans ) );
@@ -373,11 +402,10 @@ class PlanRepositoryTest extends EngineIntegrationTestCase {
}
public function test_reorder_fails_before_updates_when_an_id_is_missing_or_outside_extension(): void {
- $group_id = $this->make_group();
- $repo = new PlanRepository();
+ $repo = new PlanRepository();
- $first_id = $this->make_plan( $repo, $group_id, 'First', 'lite', 1 );
- $other_id = $this->make_plan( $repo, $group_id, 'Other', 'other-extension', 2 );
+ $first_id = $this->make_plan( $repo, 'First', 'lite', 1 );
+ $other_id = $this->make_plan( $repo, 'Other', 'other-extension', 2 );
$this->assertFalse(
$repo->reorder(
@@ -412,12 +440,11 @@ class PlanRepositoryTest extends EngineIntegrationTestCase {
}
public function test_query_ids_returns_only_those_plans(): void {
- $repo = new PlanRepository();
- $group_id = $this->make_group();
+ $repo = new PlanRepository();
- $first_plan_id = $this->make_plan( $repo, $group_id, 'First', 'lite', 1 );
- $second_plan_id = $this->make_plan( $repo, $group_id, 'Second', 'lite', 2 );
- $this->make_plan( $repo, $group_id, 'Third', 'lite', 3 );
+ $first_plan_id = $this->make_plan( $repo, 'First', 'lite', 1 );
+ $second_plan_id = $this->make_plan( $repo, 'Second', 'lite', 2 );
+ $this->make_plan( $repo, 'Third', 'lite', 3 );
$plans = $repo->query( array( 'ids' => array( $first_plan_id, $second_plan_id ) ) );
@@ -426,13 +453,12 @@ class PlanRepositoryTest extends EngineIntegrationTestCase {
}
public function test_query_ids_composes_with_status_and_extension_slugs(): void {
- $repo = new PlanRepository();
- $group_id = $this->make_group();
+ $repo = new PlanRepository();
- $active_id = $this->make_plan( $repo, $group_id, 'Active lite', 'lite', 1 );
- $foreign_id = $this->make_plan( $repo, $group_id, 'Other extension', 'other-extension', 2 );
+ $active_id = $this->make_plan( $repo, 'Active lite', 'lite', 1 );
+ $foreign_id = $this->make_plan( $repo, 'Other extension', 'other-extension', 2 );
- $archived = $repo->find( $this->make_plan( $repo, $group_id, 'Archived lite', 'lite', 3 ) );
+ $archived = $repo->find( $this->make_plan( $repo, 'Archived lite', 'lite', 3 ) );
$this->assertInstanceOf( Plan::class, $archived );
$archived->set_status( Plan::STATUS_ARCHIVED );
$this->assertTrue( $repo->update( $archived ) );
@@ -450,9 +476,8 @@ class PlanRepositoryTest extends EngineIntegrationTestCase {
}
public function test_query_empty_or_invalid_ids_match_nothing(): void {
- $repo = new PlanRepository();
- $group_id = $this->make_group();
- $plan_id = $this->make_plan( $repo, $group_id, 'Plan', 'lite' );
+ $repo = new PlanRepository();
+ $plan_id = $this->make_plan( $repo, 'Plan', 'lite' );
$this->assertCount( 0, $repo->query( array( 'ids' => array() ) ) );
$this->assertSame( 0, $repo->count( array( 'ids' => array() ) ) );
@@ -462,11 +487,10 @@ class PlanRepositoryTest extends EngineIntegrationTestCase {
}
public function test_query_null_ids_behaves_as_arg_absent(): void {
- $repo = new PlanRepository();
- $group_id = $this->make_group();
+ $repo = new PlanRepository();
- $first_plan_id = $this->make_plan( $repo, $group_id, 'First', 'lite', 1 );
- $second_plan_id = $this->make_plan( $repo, $group_id, 'Second', 'lite', 2 );
+ $first_plan_id = $this->make_plan( $repo, 'First', 'lite', 1 );
+ $second_plan_id = $this->make_plan( $repo, 'Second', 'lite', 2 );
$plans = $repo->query( array( 'ids' => null ) );
@@ -475,12 +499,10 @@ class PlanRepositoryTest extends EngineIntegrationTestCase {
}
public function test_delete_removes_the_row(): void {
- $group_id = $this->make_group();
- $repo = new PlanRepository();
+ $repo = new PlanRepository();
$id = $repo->insert(
Plan::create(
- $group_id,
array(
'name' => 'Doomed',
'billing_policy' => BillingPolicy::from_array(
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 33fe09f1c08..bb92abb6525 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
@@ -25,7 +25,6 @@ class SchemaInstallerTest extends EngineIntegrationTestCase {
*/
public function table_provider(): array {
return array(
- array( SchemaInstaller::TABLE_PLAN_GROUPS ),
array( SchemaInstaller::TABLE_PLANS ),
array( SchemaInstaller::TABLE_CONTRACTS ),
array( SchemaInstaller::TABLE_CONTRACT_ITEMS ),
diff --git a/packages/php/woocommerce-subscriptions-engine/tests/unit/Core/Entity/PlanTest.php b/packages/php/woocommerce-subscriptions-engine/tests/unit/Core/Entity/PlanTest.php
index c990347ea3a..b97611a918b 100644
--- a/packages/php/woocommerce-subscriptions-engine/tests/unit/Core/Entity/PlanTest.php
+++ b/packages/php/woocommerce-subscriptions-engine/tests/unit/Core/Entity/PlanTest.php
@@ -31,7 +31,6 @@ class PlanTest extends TestCase {
public function test_create_defaults_category_and_extension_slug(): void {
$plan = Plan::create(
- 5,
array(
'name' => 'Monthly box',
'billing_policy' => $this->billing(),
@@ -39,16 +38,44 @@ class PlanTest extends TestCase {
);
$this->assertNull( $plan->get_id() );
- $this->assertSame( 5, $plan->get_group_id() );
$this->assertSame( Plan::DEFAULT_CATEGORY, $plan->get_category() );
$this->assertSame( Plan::STATUS_ACTIVE, $plan->get_status() );
$this->assertSame( 0, $plan->get_sort_order() );
+ $this->assertNull( $plan->get_merchant_code() );
$this->assertNull( $plan->get_extension_slug() );
}
+ public function test_merchant_code_round_trips_through_create_and_storage(): void {
+ $plan = Plan::create(
+ array(
+ 'name' => 'Coded',
+ 'billing_policy' => $this->billing(),
+ 'merchant_code' => 'monthly-box',
+ )
+ );
+
+ $this->assertSame( 'monthly-box', $plan->get_merchant_code() );
+ $this->assertSame( 'monthly-box', $plan->to_storage()['merchant_code'] );
+
+ $hydrated = Plan::from_storage( $plan->to_storage() );
+
+ $this->assertSame( 'monthly-box', $hydrated->get_merchant_code() );
+ }
+
+ public function test_absent_merchant_code_is_null_in_storage(): void {
+ $plan = Plan::create(
+ array(
+ 'name' => 'Uncoded',
+ 'billing_policy' => $this->billing(),
+ )
+ );
+
+ $this->assertNull( $plan->to_storage()['merchant_code'] );
+ $this->assertNull( Plan::from_storage( $plan->to_storage() )->get_merchant_code() );
+ }
+
public function test_calculate_price_delegates_to_pricing_policy(): void {
$plan = Plan::create(
- 1,
array(
'name' => 'Discounted',
'billing_policy' => $this->billing(),
@@ -70,7 +97,6 @@ class PlanTest extends TestCase {
public function test_calculate_price_without_pricing_policy_returns_base(): void {
$plan = Plan::create(
- 1,
array(
'name' => 'Plain',
'billing_policy' => $this->billing(),
@@ -82,7 +108,6 @@ class PlanTest extends TestCase {
public function test_status_and_sort_order_are_mutable(): void {
$plan = Plan::create(
- 1,
array(
'name' => 'Ordered',
'billing_policy' => $this->billing(),
@@ -101,7 +126,6 @@ class PlanTest extends TestCase {
$this->expectException( InvalidArgumentException::class );
Plan::create(
- 1,
array(
'name' => 'Bad status',
'billing_policy' => $this->billing(),
@@ -114,7 +138,6 @@ class PlanTest extends TestCase {
$this->expectException( InvalidArgumentException::class );
Plan::create(
- 1,
array(
'name' => 'Bad',
'billing_policy' => $this->billing(),
@@ -136,7 +159,6 @@ class PlanTest extends TestCase {
$this->expectException( InvalidArgumentException::class );
Plan::create(
- 1,
array(
'name' => 'Too much',
'billing_policy' => $this->billing(),
@@ -156,7 +178,6 @@ class PlanTest extends TestCase {
public function test_to_storage_exposes_extension_slug_and_decoded_policies(): void {
$plan = Plan::create(
- 3,
array(
'name' => 'Owned',
'billing_policy' => $this->billing(),
@@ -171,7 +192,6 @@ class PlanTest extends TestCase {
$this->assertSame( 'lite', $storage['extension_slug'] );
$this->assertSame( Plan::STATUS_ARCHIVED, $storage['status'] );
$this->assertSame( 9, $storage['sort_order'] );
- $this->assertSame( 3, $storage['group_id'] );
$this->assertIsArray( $storage['billing_policy'] );
}
@@ -182,7 +202,6 @@ class PlanTest extends TestCase {
// (percentage over 100) must fail loud on hydration, not feed billing math.
Plan::from_storage(
array(
- 'group_id' => 1,
'name' => 'Corrupted',
'billing_policy' => array(
'period' => 'month',