Repository: woocommerce/woocommerce
Pull request #57429: Add order fulfillments data store
Author: login: tpaksu
Labels: name: plugin: woocommerce
description: Issues related to the WooCommerce Core plugin.
Created: 2025-04-19T22:32:17Z | Merged: 2025-04-25T15:20:20Z

### Submission Review Guidelines:

-   I have followed the [WooCommerce Contributing Guidelines](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md) and the [WordPress Coding Standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/).
-   I have checked to ensure there aren't other open [Pull Requests](https://github.com/woocommerce/woocommerce/pulls) for the same update/change.
-   I have reviewed my code for [security best practices](https://developer.wordpress.org/apis/security/).
-   Following the above guidelines will result in quick merges and clear and detailed feedback when appropriate.

<!-- You can erase any parts of this template not applicable to your Pull Request. -->

### Changes proposed in this Pull Request:

<!-- If necessary, indicate if this PR is part of a bigger feature. Add a label with the format `focus: name of the feature [team:name of the team]`. -->

<!-- Describe the changes made to this Pull Request and the reason for such changes. -->

<!-- For bug fixes: If known, please provide links to help with traceability and escape analysis. -->
<!-- Please include a link to the issue of the bug being fixed, if one doesn't exist please create it. -->
<!-- If the PR that introduced the bug is known, please also add its link below. -->

Closes #57357  .

<!-- Begin testing instructions -->

### How to test the changes in this Pull Request:

<!-- Include detailed instructions on how these changes can be tested. Review and follow the guide for how to write high-quality testing instructions. -->

New code should make sense in terms of adding a new data store, and automated tests should pass.

<!-- End testing instructions -->

### Changelog entry

<!-- You can optionally choose to enter a changelog entry by checking the box below and supplying data. -->
<!-- It will trigger the 'Add changelog to PR' CI job to create and push the entry into the branch. -->

<!-- Due to org permissions, the job might fail for PRs crated from a fork under GitHub organizations. Possible solutions: -->
<!-- * Create entry manually with `pnpm --filter='@woocommerce/plugin-woocommerce' changelog add` and push it into the branch (replace `@woocommerce/plugin-woocommerce` with package name from nearest `package.json` file) -->
<!-- * Create entry from supplied PR data and push it automatically `pnpm utils changefile pr-number-here -o github-org-name-here` -->

-   [x] Automatically create a changelog entry from the details below.

<!-- If no changelog entry is required for this PR, you can specify that below and provide a comment explaining why. This cannot be used if you selected the option to automatically create a changelog entry above. -->

-   [ ] This Pull Request does not require a changelog entry. (Comment required below)

<details>

<summary>Changelog Entry Details</summary>

#### Significance

<!-- Choose only one -->

-   [x] Patch
-   [ ] Minor
-   [ ] Major

#### Type

<!-- Choose only one -->

-   [ ] Fix - Fixes an existing bug
-   [x] Add - Adds functionality
-   [ ] Update - Update existing functionality
-   [ ] Dev - Development related task
-   [ ] Tweak - A minor adjustment to the codebase
-   [ ] Performance - Address performance issues
-   [ ] Enhancement - Improvement to existing functionality

#### Message <!-- Add a changelog message here -->

</details>

<details>

<summary>Changelog Entry Comment</summary>

#### Comment <!-- If your Pull Request doesn't require a changelog entry, a comment explaining why is required instead -->
Add order fulfillments data store to interact with the new fulfillment tables.
</details>


Linked issue #57357: [Order Fulfillments] Create data store for fulfillment tables
**Issue:** We need a data repository for accessing the created tables for the fulfillment project.

Proposed solution: Introduce a Fulfillments Data Store class to handle all possible actions outlined in [https://wooorderstatusp2.wordpress.com/2025/04/16/tsd-woocommerce-fulfillment-entity/#fulfillment-data-store-methods](https://wooorderstatusp2.wordpress.com/2025/04/16/tsd-woocommerce-fulfillment-entity/#fulfillment-data-store-methods)

Diff:
diff --git a/plugins/woocommerce/changelog/57429-add-57357-order-fulfillments-data-store-2 b/plugins/woocommerce/changelog/57429-add-57357-order-fulfillments-data-store-2
new file mode 100644
index 0000000000000..13b3cb1383a68
--- /dev/null
+++ b/plugins/woocommerce/changelog/57429-add-57357-order-fulfillments-data-store-2
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+Comment: Add order fulfillments data store to interact with the new fulfillment tables.
+
diff --git a/plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStore.php
new file mode 100644
index 0000000000000..19441806cf517
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStore.php
@@ -0,0 +1,384 @@
+<?php
+/**
+ * Class FulfillmentsDataStore file.
+ *
+ * @package WooCommerce\DataStores
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\DataStores\Fulfillments;
+
+use Automattic\WooCommerce\Internal\Fulfillments\Fulfillment;
+use WC_Meta_Data;
+use WP_Error;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * WC Order Item Product Data Store
+ *
+ * @version  9.9.0
+ */
+class FulfillmentsDataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface, FulfillmentsDataStoreInterface {
+
+	/**
+	 * Method to create a new fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to create.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data is invalid.
+	 * @throws \Exception If the fulfillment can't be created.
+	 */
+	public function create( &$data ): void {
+		// Validate the fulfillment data.
+		if ( ! $data->get_entity_type() ) {
+			throw new \Exception( esc_html__( 'Invalid entity type.', 'woocommerce' ) );
+		}
+		if ( ! $data->get_entity_id() ) {
+			throw new \Exception( esc_html__( 'Invalid entity ID.', 'woocommerce' ) );
+		}
+		if ( empty( $data->get_items() ) ) {
+			throw new \Exception( esc_html__( 'The fulfillment should contain at least one item.', 'woocommerce' ) );
+		}
+		foreach ( $data->get_items() as $item ) {
+			if ( ! isset( $item['item_id'] ) || ! isset( $item['qty'] ) ) {
+				throw new \Exception( esc_html__( 'Invalid item.', 'woocommerce' ) );
+			}
+		}
+
+		// Set fulfillment properties.
+		$data->set_date_updated( current_time( 'mysql' ) );
+
+		// Save the fulfillment to the database.
+		global $wpdb;
+		$rows_inserted = $wpdb->insert(
+			$wpdb->prefix . 'wc_order_fulfillments',
+			array(
+				'entity_type'  => $data->get_entity_type(),
+				'entity_id'    => $data->get_entity_id(),
+				'status'       => $data->get_status() ?? 'unfulfilled',
+				'is_fulfilled' => $data->get_is_fulfilled() ? 1 : 0,
+				'date_updated' => $data->get_date_updated(),
+				'date_deleted' => $data->get_date_deleted(),
+			),
+			array( '%s', '%s', '%s', '%d', '%s', '%s' )
+		);
+
+		// Check for errors.
+		if ( false === $rows_inserted ) {
+			throw new \Exception( esc_html__( 'Failed to insert fulfillment.', 'woocommerce' ) );
+		}
+
+		// Set the ID of the fulfillment object.
+		$data_id = $wpdb->insert_id;
+
+		$data->set_id( $data_id );
+
+		// Save the metadata for the fulfillment to the database.
+		$data->save_meta_data();
+
+		// Apply changes let's the object know that the current object reflects the database and no "changes" exist between the two.
+		$data->apply_changes();
+		$data->set_object_read( true );
+	}
+
+	/**
+	 * Method to read a fulfillment from the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to read.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data can't be read.
+	 */
+	public function read( &$data ): void {
+		// Read the fulfillment from the database.
+		global $wpdb;
+
+		$data_id          = $data->get_id();
+		$fulfillment_data = $wpdb->get_row(
+			$wpdb->prepare(
+				"SELECT * FROM {$wpdb->prefix}wc_order_fulfillments WHERE fulfillment_id = %d",
+				$data_id
+			),
+			ARRAY_A
+		);
+
+		if ( empty( $fulfillment_data ) ) {
+			throw new \Exception( esc_html__( 'Fulfillment not found.', 'woocommerce' ) );
+		}
+
+		$data->set_props( $fulfillment_data );
+		$data->read_meta_data( true );
+		$data->set_object_read( true );
+	}
+
+	/**
+	 * Method to update an existing fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to update.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment can't be updated.
+	 */
+	public function update( &$data ): void {
+		// Update the fulfillment in the database.
+		global $wpdb;
+
+		$data_id = $data->get_id();
+		$wpdb->update(
+			$wpdb->prefix . 'wc_order_fulfillments',
+			array(
+				'entity_type'  => $data->get_entity_type(),
+				'entity_id'    => $data->get_entity_id(),
+				'status'       => $data->get_status(),
+				'is_fulfilled' => $data->get_is_fulfilled() ? 1 : 0,
+				'date_updated' => $data->get_date_updated(),
+				'date_deleted' => $data->get_date_deleted(),
+			),
+			array( 'fulfillment_id' => $data_id ),
+			array( '%s', '%s', '%s', '%d', '%s', '%s' ),
+			array( '%d' )
+		);
+
+		// Check for errors.
+		if ( $wpdb->last_error ) {
+			throw new \Exception( esc_html__( 'Failed to update fulfillment.', 'woocommerce' ) . ' ' . esc_html( $wpdb->last_error ) );
+		}
+
+		// Update the metadata for the fulfillment.
+		$data->save_meta_data();
+		$data->apply_changes();
+
+		$data->set_object_read( true );
+	}
+
+	/**
+	 * Method to delete a fulfillment from the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to delete.
+	 * @param array       $args Optional arguments to pass to the delete method.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment can't be deleted.
+	 */
+	public function delete( &$data, $args = array() ): void {
+		// Soft Delete the fulfillment from the database.
+		global $wpdb;
+
+		$data_id       = $data->get_id();
+		$deletion_time = current_time( 'mysql' );
+		$wpdb->update(
+			$wpdb->prefix . 'wc_order_fulfillments',
+			array( 'date_deleted' => $deletion_time ),
+			array( 'fulfillment_id' => $data_id ),
+			array( '%s' ),
+			array( '%d' )
+		);
+
+		// Check for errors.
+		if ( $wpdb->last_error ) {
+			throw new \Exception( esc_html__( 'Failed to delete fulfillment.', 'woocommerce' ) );
+		}
+
+		// Set the fulfillment object to a fresh state.
+		$data = new Fulfillment();
+	}
+
+	/**
+	 * Method to read the metadata for a fulfillment.
+	 *
+	 * @param Fulfillment $data The fulfillment object to read.
+	 * @return array
+	 *
+	 * @throws \Exception If the fulfillment is not saved.
+	 */
+	public function read_meta( &$data ): array {
+		if ( ! $data->get_id() ) {
+			throw new \Exception( esc_html__( 'Invalid fulfillment.', 'woocommerce' ) );
+		}
+
+		// Read the metadata for the fulfillment.
+		global $wpdb;
+
+		$data_id   = $data->get_id();
+		$meta_data = $wpdb->get_results(
+			$wpdb->prepare(
+				"SELECT * FROM {$wpdb->prefix}wc_order_fulfillment_meta WHERE fulfillment_id = %d",
+				$data_id
+			),
+			OBJECT
+		);
+
+		return $meta_data;
+	}
+
+	/**
+	 * Method to delete the metadata for a fulfillment.
+	 *
+	 * @param Fulfillment  $data The fulfillment object to delete.
+	 * @param WC_Meta_Data $meta Meta object (containing at least ->id).
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment or meta is not saved.
+	 */
+	public function delete_meta( &$data, $meta ): void {
+		// Check if the fulfillment and meta are saved.
+		$data_id = $data->get_id();
+		$meta_id = $meta->id;
+		if ( ! is_numeric( $data_id ) || $data_id <= 0 || ! is_numeric( $meta_id ) || $meta_id <= 0 ) {
+			throw new \Exception( esc_html__( 'Invalid fulfillment or meta.', 'woocommerce' ) );
+		}
+
+		// Delete the metadata for the fulfillment.
+		global $wpdb;
+
+		$wpdb->delete(
+			$wpdb->prefix . 'wc_order_fulfillment_meta',
+			array(
+				'fulfillment_id' => $data_id,
+				'meta_id'        => $meta_id,
+			),
+			array(
+				'%d',
+				'%d',
+			)
+		);
+	}
+
+	/**
+	 * Method to add metadata for a fulfillment.
+	 *
+	 * @param Fulfillment  $data The fulfillment object to save.
+	 * @param WC_Meta_Data $meta Meta object (containing at least ->id).
+	 * @return int meta ID or WP_Error on failure.
+	 *
+	 * @throws \Exception If the fulfillment or meta is not saved.
+	 */
+	public function add_meta( &$data, $meta ): int {
+		// Add the metadata for the fulfillment.
+		global $wpdb;
+
+		// Data ID can't be something wrong as this function is called after the meta is read.
+		// See WC_Data::save_meta_data().
+		$data_id = $data->get_id();
+
+		$wpdb->insert(
+			$wpdb->prefix . 'wc_order_fulfillment_meta',
+			array(
+				'fulfillment_id' => $data_id,
+				'meta_key'       => $meta->key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+				'meta_value'     => $meta->value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
+			),
+			array(
+				'%d',
+				'%s',
+				'%s',
+			)
+		);
+
+		// Note: There is no error check on WC_Data::save_meta_data(), and it expects us to return an ID in all cases.
+		// If there's an error, we should return null to indicate we didn't save it.
+		if ( $wpdb->last_error ) {
+			throw new \Exception( esc_html__( 'Failed to insert fulfillment meta.', 'woocommerce' ) );
+		}
+
+		return $wpdb->insert_id;
+	}
+
+	/**
+	 * Method to save the metadata for a fulfillment.
+	 *
+	 * @param Fulfillment  $data The fulfillment object to save.
+	 * @param WC_Meta_Data $meta Meta object (containing at least ->id).
+	 *
+	 * @return int Number of rows updated.
+	 *
+	 * @throws \Exception If the fulfillment or meta is not saved.
+	 */
+	public function update_meta( &$data, $meta ): int {
+		// Update the metadata for the fulfillment.
+		global $wpdb;
+
+		$data_id      = $data->get_id();
+		$rows_updated = $wpdb->update(
+			$wpdb->prefix . 'wc_order_fulfillment_meta',
+			array(
+				'meta_value' => $meta->value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
+			),
+			array(
+				'fulfillment_id' => $data_id,
+				'meta_id'        => $meta->id,
+				'meta_key'       => $meta->key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+			),
+			array(
+				'%s',
+			),
+			array(
+				'%d',
+				'%d',
+				'%s',
+			)
+		);
+
+		// Check for errors.
+		if ( $wpdb->last_error ) {
+			throw new \Exception( esc_html__( 'Failed to update fulfillment meta.', 'woocommerce' ) );
+		}
+
+		return $rows_updated;
+	}
+
+	/**
+	 * Method to read the fulfillment data.
+	 *
+	 * @param string $entity_type The entity type.
+	 * @param string $entity_id The entity ID.
+	 *
+	 * @return Fulfillment[] Fulfillment object.
+	 */
+	public function read_fulfillments( string $entity_type, string $entity_id ): array {
+		// Read the fulfillment data from the database.
+		global $wpdb;
+
+		$fulfillment_data = $wpdb->get_results(
+			$wpdb->prepare(
+				"SELECT * FROM {$wpdb->prefix}wc_order_fulfillments WHERE entity_type = %s AND entity_id = %s",
+				$entity_type,
+				$entity_id
+			),
+			ARRAY_A
+		);
+
+		if ( empty( $fulfillment_data ) ) {
+			return array();
+		}
+
+		// Create Fulfillment objects from the data.
+		$fulfillments = array();
+		foreach ( $fulfillment_data as $data ) {
+			// Note: Don't initialize with ID, it will cause a re-read from the database.
+			// Set the ID directly after the object is created.
+			$fulfillment = new Fulfillment();
+			$fulfillment->set_id( $data['fulfillment_id'] );
+			$fulfillment->set_props( $data );
+			$fulfillment->apply_changes();
+			$fulfillment->set_object_read( true );
+
+			// Read the metadata for the fulfillment.
+			$fulfillment->read_meta_data( true );
+
+			$fulfillments[] = $fulfillment;
+		}
+
+		return $fulfillments;
+	}
+}
diff --git a/plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStoreInterface.php b/plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStoreInterface.php
new file mode 100644
index 0000000000000..10e80bb3348bf
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStoreInterface.php
@@ -0,0 +1,25 @@
+<?php
+/**
+ * Fulfillments Data Store Interface
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\DataStores\Fulfillments;
+
+/**
+ * Interface FulfillmentsDataStoreInterface
+ *
+ * @package Automattic\WooCommerce\Internal\DataStores\Fulfillments
+ */
+interface FulfillmentsDataStoreInterface {
+	/**
+	 * Read the fulfillment data.
+	 *
+	 * @param string $entity_type The entity type.
+	 * @param string $entity_id The entity ID.
+	 *
+	 * @return Fulfillment[] Fulfillment object.
+	 */
+	public function read_fulfillments( string $entity_type, string $entity_id ): array;
+}
diff --git a/plugins/woocommerce/src/Internal/Fulfillments/Fulfillment.php b/plugins/woocommerce/src/Internal/Fulfillments/Fulfillment.php
new file mode 100644
index 0000000000000..6ef125cfb5da2
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/Fulfillments/Fulfillment.php
@@ -0,0 +1,221 @@
+<?php
+/**
+ * WooCommerce order fulfillments.
+ *
+ * The WooCommerce order fulfillments class gets contains fulfillment related properties and methods.
+ *
+ * @package WooCommerce\Classes
+ * @version 9.9.0
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\Fulfillments;
+
+use Automattic\WooCommerce\Internal\DataStores\Fulfillments\FulfillmentsDataStore;
+
+defined( 'ABSPATH' ) || exit;
+
+
+/**
+ * WC Order Fulfillment Class
+ *
+ * @since 9.9.0
+ */
+class Fulfillment extends \WC_Data {
+	/**
+	 * Fulfillment constructor. Loads fulfillment data.
+	 *
+	 * @param array|string|Fulfillment $data Fulfillment data.
+	 */
+	public function __construct( $data = '' ) {
+		parent::__construct( $data );
+
+		if ( $data instanceof Fulfillment ) {
+			$this->set_id( absint( $data->get_id() ) );
+		} elseif ( is_numeric( $data ) ) {
+			$this->set_id( absint( $data ) );
+		} elseif ( is_array( $data ) && isset( $data['id'] ) ) {
+			$this->set_id( absint( $data['id'] ) );
+		} elseif ( is_string( $data ) && ! empty( $data ) ) {
+			$this->set_id( absint( $data ) );
+		} elseif ( is_object( $data ) && isset( $data->id ) ) {
+			$this->set_id( absint( $data->id ) );
+		} else {
+			$this->set_object_read( true );
+		}
+
+		// Load the items array.
+		$this->data_store = wc_get_container()->get( FulfillmentsDataStore::class );
+		if ( $this->get_id() > 0 ) {
+			$this->data_store->read( $this );
+		}
+	}
+
+	/**
+	 * Get the fulfillment ID.
+	 *
+	 * @return int Fulfillment ID.
+	 */
+	public function get_id(): int {
+		return $this->data['fulfillment_id'] ?? 0;
+	}
+
+	/**
+	 * Set the fulfillment ID.
+	 *
+	 * @param int $id Fulfillment ID.
+	 */
+	public function set_id( $id ): void {
+		$this->data['fulfillment_id'] = is_numeric( $id ) ? absint( $id ) : 0;
+		parent::set_id( $this->data['fulfillment_id'] );
+	}
+
+	/**
+	 * Get the entity type.
+	 *
+	 * @return string|null Entity type.
+	 */
+	public function get_entity_type(): ?string {
+		return $this->data['entity_type'] ?? null;
+	}
+
+	/**
+	 * Set the entity type.
+	 *
+	 * @param class-string|null $entity_type Entity type.
+	 */
+	public function set_entity_type( ?string $entity_type ): void {
+		$this->data['entity_type'] = $entity_type;
+	}
+
+	/**
+	 * Get the entity ID.
+	 *
+	 * @return string|null Entity ID.
+	 */
+	public function get_entity_id(): ?string {
+		return $this->data['entity_id'] ?? null;
+	}
+
+	/**
+	 * Set the entity ID.
+	 *
+	 * @param class-string|null $entity_id Entity ID.
+	 */
+	public function set_entity_id( ?string $entity_id ): void {
+		$this->data['entity_id'] = $entity_id;
+	}
+
+	/**
+	 * Set fulfillment status.
+	 *
+	 * @param string|null $status Fulfillment status.
+	 *
+	 * @return void
+	 */
+	public function set_status( ?string $status ): void {
+		$this->data['status'] = $status;
+	}
+
+	/**
+	 * Get the fulfillment status.
+	 *
+	 * @return string|null Fulfillment status.
+	 */
+	public function get_status(): ?string {
+		return $this->data['status'] ?? null;
+	}
+
+	/**
+	 * Set if the fulfillment is fulfilled.
+	 *
+	 * @param bool $is_fulfilled Whether the fulfillment is fulfilled.
+	 *
+	 *  @return void
+	 */
+	public function set_is_fulfilled( bool $is_fulfilled ): void {
+		$this->data['is_fulfilled'] = $is_fulfilled;
+	}
+
+	/**
+	 * Get if the fulfillment is fulfilled.
+	 *
+	 * @return bool Whether the fulfillment is fulfilled.
+	 */
+	public function get_is_fulfilled(): bool {
+		return $this->data['is_fulfilled'] ?? false;
+	}
+
+	/**
+	 * Get the date updated.
+	 *
+	 * @return string|null Date updated.
+	 */
+	public function get_date_updated(): ?string {
+		return $this->data['date_updated'] ?? null;
+	}
+
+	/**
+	 * Set the date updated.
+	 *
+	 * @param string|null $date_updated Date updated.
+	 */
+	public function set_date_updated( ?string $date_updated ) {
+		$this->data['date_updated'] = $date_updated;
+	}
+	/**
+	 * Get the date deleted.
+	 *
+	 * @return string|null Date deleted.
+	 */
+	public function get_date_deleted(): ?string {
+		return $this->data['date_deleted'] ?? null;
+	}
+	/**
+	 * Set the date deleted.
+	 *
+	 * @param string|null $date_deleted Date deleted.
+	 * @return void
+	 */
+	public function set_date_deleted( ?string $date_deleted ): void {
+		$this->data['date_deleted'] = $date_deleted;
+	}
+
+	/**
+	 * Get the fulfillment items.
+	 *
+	 * @return array Fulfillment items.
+	 */
+	public function get_items(): array {
+		// Get the meta data for this fulfillment.
+		$meta_data = $this->get_meta_data();
+
+		// Get the meta object with the "_items" meta key.
+		$items = array_values(
+			array_filter(
+				$meta_data,
+				function ( $meta ) {
+					return '_items' === $meta->key;
+				}
+			)
+		);
+
+		// If we have a matching meta key, decode the JSON and return it.
+		if ( 0 < count( $items ) ) {
+			return json_decode( $items[0]->value, true );
+		}
+
+		// If we don't have a matching meta key, return an empty array.
+		return array();
+	}
+
+	/**
+	 * Set the fulfillment items.
+	 *
+	 * @param array $items Fulfillment items.
+	 */
+	public function set_items( array $items ): void {
+		$this->update_meta_data( '_items', wp_json_encode( $items ) );
+	}
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Fulfillments/FulfillmentsDataStoreTest.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Fulfillments/FulfillmentsDataStoreTest.php
new file mode 100644
index 0000000000000..f826d44872425
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Fulfillments/FulfillmentsDataStoreTest.php
@@ -0,0 +1,632 @@
+<?php
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\DataStores\Fulfillments;
+
+use Automattic\WooCommerce\Internal\DataStores\Fulfillments\FulfillmentsDataStore;
+use Automattic\WooCommerce\Internal\Fulfillments\Fulfillment;
+use WC_Meta_Data;
+
+/**
+ * Tests for the WC_Order_Fulfillment_Data_Store_Test  class.
+ *
+ * @package WooCommerce\Tests\Order_Fulfillment
+ */
+class FulfillmentsDataStoreTest extends \WC_Unit_Test_Case {
+	/**
+	 * The instance of the order fulfillment data store to use.
+	 *
+	 * @var FulfillmentsDataStore
+	 */
+	private static FulfillmentsDataStore $order_fulfillment_data_store;
+
+	/**
+	 * Runs before all the tests of the class.
+	 */
+	public static function setUpBeforeClass(): void {
+		parent::setUpBeforeClass();
+
+		self::$order_fulfillment_data_store = new FulfillmentsDataStore();
+	}
+
+	/**
+	 * Tests the create method of the order fulfillment data store.
+	 */
+	public function test_create_fulfillment() {
+		$fulfillment = new Fulfillment();
+		$fulfillment->set_entity_type( 'order-fulfillment' );
+		$fulfillment->set_entity_id( '123' );
+		$fulfillment->set_items(
+			array(
+				array(
+					'item_id' => 1,
+					'qty'     => 2,
+				),
+				array(
+					'item_id' => 2,
+					'qty'     => 3,
+				),
+			)
+		);
+
+		self::$order_fulfillment_data_store->create( $fulfillment );
+		$this->assertFulfillmentRecordInDB( $fulfillment );
+		$this->assertFulfillmentMetaInDB( $fulfillment );
+	}
+
+	/**
+	 * Tests the create method of the order fulfillment data store with invalid entity type.
+	 */
+	public function test_create_fulfillment_throws_error_on_invalid_entity_type() {
+		$fulfillment = new Fulfillment();
+		$fulfillment->set_entity_type( '' );
+		$fulfillment->set_entity_id( '123' );
+		$fulfillment->set_items(
+			array(
+				array(
+					'item_id' => 1,
+					'qty'     => 2,
+				),
+			)
+		);
+
+		$this->expectException( \Exception::class );
+		$this->expectExceptionMessage( 'Invalid entity type.' );
+
+		self::$order_fulfillment_data_store->create( $fulfillment );
+	}
+
+	/**
+	 * Tests the create method of the order fulfillment data store with invalid entity ID.
+	 */
+	public function test_create_fulfillment_throws_error_on_invalid_entity_id() {
+		$fulfillment = new Fulfillment();
+		$fulfillment->set_entity_type( 'order-fulfillment' );
+		$fulfillment->set_entity_id( '' );
+		$fulfillment->set_items(
+			array(
+				array(
+					'item_id' => 1,
+					'qty'     => 2,
+				),
+			)
+		);
+
+		$this->expectException( \Exception::class );
+		$this->expectExceptionMessage( 'Invalid entity ID.' );
+
+		self::$order_fulfillment_data_store->create( $fulfillment );
+	}
+
+	/**
+	 * Tests the create method of the order fulfillment data store with invalid items.
+	 */
+	public function test_create_fulfillment_throws_error_on_invalid_items() {
+		$fulfillment = new Fulfillment();
+		$fulfillment->set_entity_type( 'order-fulfillment' );
+		$fulfillment->set_entity_id( '123' );
+		$fulfillment->set_props( array( 'meta_data' => array( '_items' => null ) ) );
+
+		$this->expectException( \Exception::class );
+		$this->expectExceptionMessage( 'The fulfillment should contain at least one item.' );
+
+		self::$order_fulfillment_data_store->create( $fulfillment );
+	}
+
+	/**
+	 * Tests the create method of the order fulfillment data store with no items.
+	 */
+	public function test_create_fulfillment_throws_error_on_empty_items() {
+		$fulfillment = new Fulfillment();
+		$fulfillment->set_entity_type( 'order-fulfillment' );
+		$fulfillment->set_entity_id( '123' );
+		$fulfillment->set_items( array() );
+
+		$this->expectException( \Exception::class );
+		$this->expectExceptionMessage( 'The fulfillment should contain at least one item.' );
+
+		self::$order_fulfillment_data_store->create( $fulfillment );
+	}
+
+	/**
+	 * Tests the create method of the order fulfillment data store with invalid item.
+	 */
+	public function test_create_fulfillment_throws_error_on_invalid_item() {
+		$fulfillment = new Fulfillment();
+		$fulfillment->set_entity_type( 'order-fulfillment' );
+		$fulfillment->set_entity_id( '123' );
+		$fulfillment->set_items(
+			array(
+				array(
+					'item_id' => 1,
+					'qty'     => 2,
+				),
+				array(
+					'item_id' => 2,
+					// Missing qty.
+				),
+			)
+		);
+
+		$this->expectException( \Exception::class );
+		$this->expectExceptionMessage( 'Invalid item.' );
+
+		self::$order_fulfillment_data_store->create( $fulfillment );
+	}
+
+	/**
+	 * Tests the read method of the order fulfillment data store.
+	 */
+	public function test_read_fulfillment() {
+		$fulfillment = new Fulfillment();
+		$fulfillment->set_entity_type( 'order-fulfillment' );
+		$fulfillment->set_entity_id( '123' );
+		$fulfillment->set_items(
+			array(
+				array(
+					'item_id' => 1,
+					'qty'     => 2,
+				),
+				array(
+					'item_id' => 2,
+					'qty'     => 3,
+				),
+			)
+		);
+		self::$order_fulfillment_data_store->create( $fulfillment );
+
+		$this->assertNotNull( $fulfillment->get_id() );
+
+		$new_fulfillment = new Fulfillment();
+		$new_fulfillment->set_id( $fulfillment->get_id() );
+
+		self::$order_fulfillment_data_store->read( $new_fulfillment );
+
+		$this->assertFulfillmentRecordInDB( $new_fulfillment );
+		$this->assertFulfillmentMetaInDB( $new_fulfillment );
+	}
+
+	/**
+	 * Tests the update method of the order fulfillment data store.
+	 */
+	public function test_update_fulfillment() {
+		$fulfillment = new Fulfillment();
+		$fulfillment->set_id( 1 );
+		$fulfillment->set_entity_type( 'order-fulfillment' );
+		$fulfillment->set_entity_id( '123' );
+		$fulfillment->set_items(
+			array(
+				array(
+					'item_id' => 1,
+					'qty'     => 2,
+				),
+				array(
+					'item_id' => 2,
+					'qty'     => 3,
+				),
+			)
+		);
+		self::$order_fulfillment_data_store->create( $fulfillment );
+
+		$fulfillment->set_entity_id( '456' );
+		$fulfillment->set_items(
+			array(
+				array(
+					'item_id' => 3,
+					'qty'     => 4,
+				),
+				array(
+					'item_id' => 4,
+					'qty'     => 5,
+				),
+			)
+		);
+
+		self::$order_fulfillment_data_store->update( $fulfillment );
+
+		$this->assertFulfillmentRecordInDB( $fulfillment );
+		$this->assertFulfillmentMetaInDB( $fulfillment );
+	}
+
+	/**
+	 * Tests the delete method of the order fulfillment data store.
+	 */
+	public function test_delete_fulfillment() {
+		$fulfillment = new Fulfillment();
+		$fulfillment->set_id( 1 );
+		$fulfillment->set_entity_type( 'order-fulfillment' );
+		$fulfillment->set_entity_id( '123' );
+		$fulfillment->set_items(
+			array(
+				array(
+					'item_id' => 1,
+					'qty'     => 2,
+				),
+				array(
+					'item_id' => 2,
+					'qty'     => 3,
+				),
+			)
+		);
+		self::$order_fulfillment_data_store->create( $fulfillment );
+
+		$this->assertNotNull( $fulfillment->get_id() );
+		$this->assertNull( $fulfillment->get_date_deleted() );
+
+		// Cache the metadata before deletion.
+		$metadata = $fulfillment->get_meta_data();
+
+		// Cache the ID before deletion.
+		$fulfillment_id = $fulfillment->get_id();
+
+		self::$order_fulfillment_data_store->delete( $fulfillment );
+		// The fulfillment should be reset to it's initial state.
+		$this->assertEquals( 0, $fulfillment->get_id() );
+		$this->assertEquals( null, $fulfillment->get_entity_type() );
+		$this->assertEquals( null, $fulfillment->get_entity_id() );
+		$this->assertEquals( array(), $fulfillment->get_items() );
+		$this->assertEquals( array(), $fulfillment->get_meta_data() );
+		$this->assertEquals( null, $fulfillment->get_date_updated() );
+		$this->assertEquals( null, $fulfillment->get_date_deleted() );
+		$this->assertFulfillmentRecordInDB( $fulfillment, $fulfillment_id, true );
+		$this->assertFulfillmentMetaInDB( $fulfillment, $fulfillment_id, $metadata );
+	}
+
+	/**
+	 * Tests the read_meta method of the order fulfillment data store.
+	 */
+	public function test_read_fulfillment_meta() {
+		$items = array(
+			array(
+				'item_id' => 1,
+				'qty'     => 2,
+			),
+			array(
+				'item_id' => 2,
+				'qty'     => 3,
+			),
+		);
+
+		$fulfillment = new Fulfillment();
+		$fulfillment->set_entity_id( '123' );
+		$fulfillment->set_entity_type( 'order-fulfillment' );
+
+		$fulfillment->set_items( $items );
+		$fulfillment->save();
+
+		$this->assertNotEquals( 0, $fulfillment->get_id() );
+
+		$result = self::$order_fulfillment_data_store->read_meta( $fulfillment );
+
+		$this->assertIsArray( $result );
+		$this->assertCount( 1, $result );
+		$this->assertIsObject( $result[0] );
+		$this->assertEquals( wp_json_encode( $items ), $result[0]->meta_value );
+		$this->assertEquals( '_items', $result[0]->meta_key );
+		$this->assertEquals( $fulfillment->get_id(), $result[0]->fulfillment_id );
+	}
+
+
+	/**
+	 * Tests the delete_meta method of the order fulfillment data store.
+	 */
+	public function test_delete_fulfillment_meta() {
+		$items = array(
+			array(
+				'item_id' => 1,
+				'qty'     => 2,
+			),
+			array(
+				'item_id' => 2,
+				'qty'     => 3,
+			),
+		);
+
+		$fulfillment = new Fulfillment();
+		$fulfillment->set_entity_id( '123' );
+		$fulfillment->set_entity_type( 'order-fulfillment' );
+		$fulfillment->set_items( $items );
+		$fulfillment->save();
+
+		$this->assertNotEquals( 0, $fulfillment->get_id() );
+
+		$meta = $fulfillment->get_meta_data();
+		$this->assertCount( 1, $meta );
+		$this->assertEquals( '_items', $meta[0]->key );
+		$this->assertEquals( wp_json_encode( $items ), $meta[0]->value );
+		$this->assertNotNull( $meta[0]->id );
+
+		self::$order_fulfillment_data_store->delete_meta( $fulfillment, $meta[0] ); // phpcs:ignore
+
+		global $wpdb;
+		$db_metadata = $wpdb->get_results(
+			$wpdb->prepare(
+				"SELECT * FROM {$wpdb->prefix}wc_order_fulfillment_meta WHERE fulfillment_id = %d",
+				$fulfillment->get_id()
+			)
+		);
+		$this->assertCount( 0, $db_metadata );
+	}
+
+	/**
+	 * Tests the add_meta method of the order fulfillment data store.
+	 */
+	public function test_add_fulfillment_meta() {
+		$items = array(
+			array(
+				'item_id' => 1,
+				'qty'     => 2,
+			),
+			array(
+				'item_id' => 2,
+				'qty'     => 3,
+			),
+		);
+
+		$fulfillment = new Fulfillment();
+		$fulfillment->set_entity_id( '123' );
+		$fulfillment->set_entity_type( 'order-fulfillment' );
+
+		$fulfillment->set_items( $items );
+		$fulfillment->save();
+
+		$this->assertNotEquals( 0, $fulfillment->get_id() );
+
+		self::$order_fulfillment_data_store->add_meta(
+			$fulfillment,
+			new WC_Meta_Data(
+				array(
+					'key'   => '_new_meta_key',
+					'value' => 'new_meta_value',
+				)
+			)
+		);
+
+		global $wpdb;
+		$db_metadata = $wpdb->get_results(
+			$wpdb->prepare(
+				"SELECT * FROM {$wpdb->prefix}wc_order_fulfillment_meta WHERE fulfillment_id = %d",
+				$fulfillment->get_id()
+			)
+		);
+		foreach ( $db_metadata as $meta ) {
+			if ( '_new_meta_key' === $meta->meta_key ) {
+				break;
+			}
+		}
+
+		if ( ! isset( $meta ) ) {
+			self::fail( 'Meta not found in database.' );
+			return;
+		}
+
+		self::assertEquals( 'new_meta_value', $meta->meta_value );
+	}
+
+	/**
+	 * Tests the update_meta method of the order fulfillment data store.
+	 */
+	public function test_update_fulfillment_meta() {
+		$items = array(
+			array(
+				'item_id' => 1,
+				'qty'     => 2,
+			),
+			array(
+				'item_id' => 2,
+				'qty'     => 3,
+			),
+		);
+
+		$fulfillment = new Fulfillment();
+		$fulfillment->set_entity_id( '123' );
+		$fulfillment->set_entity_type( 'order-fulfillment' );
+		$fulfillment->set_items( $items );
+		$fulfillment->save();
+
+		$this->assertNotEquals( 0, $fulfillment->get_id() );
+		$new_items = array(
+			array(
+				'item_id' => 3,
+				'qty'     => 4,
+			),
+			array(
+				'item_id' => 4,
+				'qty'     => 5,
+			),
+		);
+		$fulfillment->set_items( $new_items );
+		$new_metadata = $fulfillment->get_meta_data();
+		$this->assertCount( 1, $new_metadata );
+		$this->assertEquals( '_items', $new_metadata[0]->key );
+
+		$result = self::$order_fulfillment_data_store->update_meta( $fulfillment, $new_metadata[0] );
+
+		$this->assertEquals( 1, $result );
+	}
+
+	/**
+	 * Tests reading multiple fulfillments.
+	 */
+	public function test_read_fulfillments() {
+		$this->prepare_db_for_test();
+		$fulfillments = self::$order_fulfillment_data_store->read_fulfillments( 'order-fulfillment', '123' );
+		$this->assertCount( 2, $fulfillments );
+		$this->assertEquals( '123', $fulfillments[0]->get_entity_id() );
+		$this->assertEquals( 'order-fulfillment', $fulfillments[0]->get_entity_type() );
+		$this->assertEquals(
+			array(
+				array(
+					'item_id' => 1,
+					'qty'     => 2,
+				),
+
+			),
+			$fulfillments[0]->get_items(),
+		);
+		$this->assertEquals( '123', $fulfillments[1]->get_entity_id() );
+		$this->assertEquals( 'order-fulfillment', $fulfillments[1]->get_entity_type() );
+		$this->assertEquals(
+			array(
+				array(
+					'item_id' => 4,
+					'qty'     => 5,
+				),
+			),
+			$fulfillments[1]->get_items(),
+		);
+	}
+
+	/**
+	 * Create a test fulfillment and save it to the database.
+	 *
+	 * @param string $entity_type The entity type.
+	 * @param string $entity_id The entity ID.
+	 * @param array  $items The items to fulfill.
+	 *
+	 * @return Fulfillment The created fulfillment object.
+	 */
+	private function create_test_fulfillment( string $entity_type, string $entity_id, array $items ) {
+		$fulfillment = new Fulfillment();
+		$fulfillment->set_id( 0 );
+		$fulfillment->set_entity_type( $entity_type );
+		$fulfillment->set_entity_id( $entity_id );
+		$fulfillment->set_items( $items );
+		$fulfillment->save();
+		$fulfillment->save_meta_data();
+
+		$this->assertNotEquals( 0, $fulfillment->get_id() );
+
+		$this->assertFulfillmentRecordInDB( $fulfillment );
+		$this->assertFulfillmentMetaInDB( $fulfillment );
+
+		return $fulfillment;
+	}
+
+	/**
+	 * Creates fulfillment records in the database for testing.
+	 */
+	private function prepare_db_for_test() {
+		$this->create_test_fulfillment(
+			'order-fulfillment',
+			'123',
+			array(
+				array(
+					'item_id' => 1,
+					'qty'     => 2,
+				),
+			)
+		);
+		$this->create_test_fulfillment(
+			'order-fulfillment',
+			'456',
+			array(
+				array(
+					'item_id' => 2,
+					'qty'     => 3,
+				),
+			)
+		);
+		$this->create_test_fulfillment(
+			'order-fulfillment',
+			'789',
+			array(
+				array(
+					'item_id' => 3,
+					'qty'     => 4,
+				),
+			)
+		);
+		$this->create_test_fulfillment(
+			'order-fulfillment',
+			'123',
+			array(
+				array(
+					'item_id' => 4,
+					'qty'     => 5,
+				),
+			)
+		);
+		$this->create_test_fulfillment(
+			'order-fulfillment',
+			'456',
+			array(
+				array(
+					'item_id' => 5,
+					'qty'     => 6,
+				),
+			)
+		);
+	}
+
+	/**
+	 * Asserts that a fulfillment record exists in the database.
+	 *
+	 * @param Fulfillment $fulfillment The fulfillment object.
+	 * @param int         $deleted_id  The ID of the deleted record.
+	 * @param bool        $is_deleted  Whether the record is deleted.
+	 */
+	private function assertFulfillmentRecordInDB( Fulfillment $fulfillment, int $deleted_id = 0, bool $is_deleted = false ) {
+		global $wpdb;
+
+		$fulfillment_id = $is_deleted ? $deleted_id : $fulfillment->get_id();
+		$record         = $wpdb->get_row(
+			$wpdb->prepare(
+				"SELECT * FROM {$wpdb->prefix}wc_order_fulfillments WHERE fulfillment_id = %d",
+				$fulfillment_id
+			)
+		);
+
+		if ( ! $is_deleted ) {
+			$this->assertNotNull( $record );
+			$this->assertEquals( $fulfillment->get_entity_type(), $record->entity_type );
+			$this->assertEquals( $fulfillment->get_entity_id(), $record->entity_id );
+			$this->assertEquals( $fulfillment->get_date_updated(), $record->date_updated );
+			$this->assertEquals( $fulfillment->get_date_deleted(), $record->date_deleted );
+		} else {
+			$this->assertNotNull( $record );
+			$this->assertNotEquals( $fulfillment->get_id(), $record->fulfillment_id );
+			$this->assertNotEquals( null, $record->date_deleted );
+		}
+	}
+
+	/**
+	 * Asserts that a fulfillment record metadata matches the expected value.
+	 *
+	 * @param Fulfillment $fulfillment The fulfillment object.
+	 * @param int         $deleted_id  The ID of the deleted record, if deleted.
+	 * @param array|null  $metadata    The metadata to check.
+	 */
+	private function assertFulfillmentMetaInDB( Fulfillment $fulfillment, int $deleted_id = 0, ?array $metadata = null ) {
+		global $wpdb;
+
+		$fulfillment_id = 0 === $deleted_id ? $fulfillment->get_id() : $deleted_id;
+
+		if ( null === $metadata ) {
+			$metadata = $fulfillment->get_meta_data();
+		}
+
+		$records = $wpdb->get_results(
+			$wpdb->prepare(
+				"SELECT * FROM {$wpdb->prefix}wc_order_fulfillment_meta WHERE fulfillment_id = %d",
+				$fulfillment_id,
+			),
+			OBJECT
+		);
+
+		foreach ( $metadata as $meta ) {
+			$meta_key   = $meta->key;
+			$meta_value = $meta->value;
+			$record     = array_filter(
+				$records,
+				function ( $record ) use ( $meta_key ) {
+					return $record->meta_key === $meta_key;
+				}
+			);
+
+			$this->assertNotEmpty( $record, "$meta_key is empty" );
+			$this->assertEquals( $meta_value, reset( $record )->meta_value );
+		}
+	}
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Fulfillments/FulfillmentTest.php b/plugins/woocommerce/tests/php/src/Internal/Fulfillments/FulfillmentTest.php
new file mode 100644
index 0000000000000..0a020ec1771e9
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/Fulfillments/FulfillmentTest.php
@@ -0,0 +1,211 @@
+<?php declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\Fulfillments;
+
+use Automattic\WooCommerce\Internal\Fulfillments\Fulfillment;
+
+/**
+ * Tests for Fulfillment object.
+ */
+class FulfillmentTest extends \WC_Unit_Test_Case {
+	/**
+	 * Test that the Fulfillment object can be created.
+	 */
+	public function test_fulfillment_object() {
+		$fulfillment = new Fulfillment();
+		$this->assertInstanceOf( Fulfillment::class, $fulfillment );
+	}
+
+	/**
+	 * Test that the Fulfillment object can be created with an ID.
+	 */
+	public function test_fulfillment_object_with_id_fetches_data_and_metadata() {
+		$db_fulfillment = $this->helper_create_fulfillment();
+		$fulfillment    = new Fulfillment( $db_fulfillment->get_id() );
+
+		$this->assertInstanceOf( Fulfillment::class, $fulfillment );
+		$this->assertEquals( $db_fulfillment->get_id(), $fulfillment->get_id() );
+		$this->assertEquals( $db_fulfillment->get_entity_type(), $fulfillment->get_entity_type() );
+		$this->assertEquals( $db_fulfillment->get_entity_id(), $fulfillment->get_entity_id() );
+		$this->assertEquals( $db_fulfillment->get_date_updated(), $fulfillment->get_date_updated() );
+		$this->assertEquals( $db_fulfillment->get_date_deleted(), $fulfillment->get_date_deleted() );
+		$this->assertEquals( $db_fulfillment->get_items(), $fulfillment->get_items() );
+		$this->assertEquals( $db_fulfillment->get_meta_data(), $fulfillment->get_meta_data() );
+	}
+
+	/**
+	 * Test that Fulfillment object can be updated.
+	 */
+	public function test_fulfillment_object_update() {
+		$fulfillment = $this->helper_create_fulfillment(
+			array(
+				'entity_type' => 'order-fulfillment',
+				'entity_id'   => 123,
+			)
+		);
+
+		$fulfillment->set_entity_type( 'updated-entity-type' );
+		$fulfillment->set_entity_id( '456' );
+		$fulfillment->save();
+
+		$this->assertEquals( 'updated-entity-type', $fulfillment->get_entity_type() );
+		$this->assertEquals( 456, $fulfillment->get_entity_id() );
+	}
+
+	/**
+	 * Test that Fulfillment object can be soft deleted.
+	 */
+	public function test_fulfillment_object_soft_delete() {
+		$fulfillment = $this->helper_create_fulfillment(
+			array(
+				'entity_type' => 'order-fulfillment',
+				'entity_id'   => 123,
+			)
+		);
+
+		$fulfillment_id = $fulfillment->get_id();
+		$this->assertNotEquals( 0, $fulfillment_id );
+
+		$fulfillment->delete();
+
+		$fresh_fulfillment = new Fulfillment( $fulfillment_id );
+		$this->assertInstanceOf( Fulfillment::class, $fresh_fulfillment );
+		$this->assertNotEquals( null, $fresh_fulfillment->get_date_deleted() );
+	}
+
+	/**
+	 * Test that Fulfillment object can be created with items.
+	 */
+	public function test_fulfillment_object_with_items() {
+		$fulfillment = $this->helper_create_fulfillment(
+			array(
+				'entity_type' => 'order-fulfillment',
+				'entity_id'   => 123,
+			)
+		);
+
+		$items = array(
+			array(
+				'item_id' => 1,
+				'qty'     => 2,
+			),
+			array(
+				'item_id' => 2,
+				'qty'     => 3,
+			),
+		);
+
+		$fulfillment->set_items( $items );
+		$fulfillment->save();
+
+		$fresh_fulfillment = new Fulfillment( $fulfillment->get_id() );
+		$this->assertInstanceOf( Fulfillment::class, $fresh_fulfillment );
+		$this->assertEquals( $fulfillment->get_id(), $fresh_fulfillment->get_id() );
+
+		$this->assertEquals( $items, $fresh_fulfillment->get_items() );
+	}
+
+	/**
+	 * Test that Fulfillment object can be created with metadata.
+	 */
+	public function test_fulfillment_object_with_metadata() {
+		$fulfillment = $this->helper_create_fulfillment(
+			array(
+				'entity_type' => 'order-fulfillment',
+				'entity_id'   => 123,
+			)
+		);
+
+		$fulfillment->add_meta_data( 'test_meta_key', 'test_meta_value', true );
+		$fulfillment->save();
+
+		$this->assertEquals( 'test_meta_value', $fulfillment->get_meta( 'test_meta_key' ) );
+	}
+
+	/**
+	 * Test that metadata can be updated.
+	 */
+	public function test_fulfillment_object_update_metadata() {
+		$fulfillment = $this->helper_create_fulfillment(
+			array(
+				'entity_type' => 'order-fulfillment',
+				'entity_id'   => 123,
+			)
+		);
+
+		$fulfillment->add_meta_data( 'test_meta_key', 'test_meta_value', true );
+		$fulfillment->save();
+
+		$fulfillment->update_meta_data( 'test_meta_key', 'updated_meta_value' );
+		$fulfillment->save();
+
+		$this->assertEquals( 'updated_meta_value', $fulfillment->get_meta( 'test_meta_key' ) );
+	}
+
+	/**
+	 * Test that metadata can be deleted.
+	 */
+	public function test_fulfillment_object_delete_metadata() {
+		$fulfillment = $this->helper_create_fulfillment(
+			array(
+				'entity_type' => 'order-fulfillment',
+				'entity_id'   => 123,
+			)
+		);
+
+		$fulfillment->add_meta_data( 'test_meta_key', 'test_meta_value', true );
+		$fulfillment->save();
+
+		$fulfillment->delete_meta_data( 'test_meta_key' );
+		$fulfillment->save();
+
+		$this->assertEquals( '', $fulfillment->get_meta( 'test_meta_key' ) );
+	}
+
+	/**
+	 * Helper function to create a fulfillment.
+	 *
+	 * @param array $args Arguments to create the fulfillment.
+	 * @return Fulfillment The created fulfillment object.
+	 */
+	private function helper_create_fulfillment( $args = array() ) {
+		$fulfillment = new Fulfillment();
+		$fulfillment->set_props(
+			array_merge(
+				array(
+					'id'          => 0,
+					'entity_type' => 'order-fulfillment',
+					'entity_id'   => 123,
+				),
+				$args
+			)
+		);
+
+		$fulfillment->add_meta_data(
+			'test_meta_key',
+			'test_meta_value',
+			true
+		);
+
+		$fulfillment->set_items(
+			array(
+				array(
+					'item_id' => 1,
+					'qty'     => 2,
+				),
+				array(
+					'item_id' => 2,
+					'qty'     => 3,
+				),
+			)
+		);
+
+		$fulfillment->save();
+
+		// Check if the fulfillment was created successfully.
+		$this->assertNotEquals( 0, $fulfillment->get_id() );
+		$this->assertNotNull( $fulfillment->get_date_updated() );
+
+		return $fulfillment;
+	}
+}


Reviews:
user: login: prettyboymp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T20:54:40Z
user: login: tpaksu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T20:57:46Z
user: login: tpaksu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T20:58:42Z
user: login: tpaksu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T22:00:00Z
user: login: tpaksu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T22:00:19Z
user: login: tpaksu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T22:25:03Z
user: login: prettyboymp
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T15:25:21Z
user: login: tpaksu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T19:48:33Z
user: login: tpaksu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T19:50:35Z
user: login: tpaksu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T00:21:23Z
user: login: tpaksu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T00:53:48Z
user: login: tpaksu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T00:54:06Z
user: login: tpaksu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T00:54:14Z
user: login: tpaksu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T00:54:31Z
user: login: prettyboymp
body: Thanks for the updates.  🚢 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T11:55:47Z

Review comments:
diff_hunk: @@ -0,0 +1,401 @@
+<?php
+/**
+ * Class FulfillmentsDataStore file.
+ *
+ * @package WooCommerce\DataStores
+ */
+
+namespace Automattic\WooCommerce\Internal\DataStores\Fulfillments;
+
+use Automattic\WooCommerce\Internal\Fulfillments\Fulfillment;
+use WP_Error;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * WC Order Item Product Data Store
+ *
+ * @version  9.9.0
+ */
+class FulfillmentsDataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface {
+
+	/**
+	 * Error object.
+	 *
+	 * @var WP_Error|null $error The error object.
+	 */
+	protected $error = null;
+
+	/**
+	 * Method to create a new fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to create.
+	 *
+	 * @return void
+	 */
+	public function create( &$data ) {
+		$this->clear_error();
+
+		// Validate the fulfillment data.
+		if ( ! $data->get_entity_type() ) {
+			$this->set_error( new WP_Error( 'invalid_entity_type', __( 'Invalid entity type.', 'woocommerce' ) ) );
path: plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStore.php
user: login: prettyboymp
body: I'm a little concerned that we're introducing a different pattern here for this datastore where a client needs to check for errors explicitly after write commands.   It is probably safest to have an exception be thrown so execution doesn't continue with the assumption that everything went okay.
created_at: 2025-04-22T19:42:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,200 @@
+<?php
+/**
+ * WooCommerce order fulfillments.
+ *
+ * The WooCommerce order fulfillments class gets contains fulfillment related properties and methods.
+ *
+ * @package WooCommerce\Classes
+ * @version 9.9.0
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\Fulfillments;
+
+use Automattic\WooCommerce\Internal\DataStores\Fulfillments\FulfillmentsDataStore;
+
+defined( 'ABSPATH' ) || exit;
+
+
+/**
+ * WC Order Fulfillment Class
+ *
+ * @since 9.9.0
+ */
+class Fulfillment extends \WC_Data {
+	/**
+	 * Fulfillment ID.
+	 *
+	 * @var int
+	 */
+	protected $fulfillment_id;
+
+	/**
+	 * Entity type. Will be referencing an object type on the system. For WC, this is WC_Order.
+	 *
+	 * @var string
+	 */
+	protected $entity_type;
+
+	/**
+	 * Entity ID. Can be string, int, or UUID according to the entity used with this record.
+	 *
+	 * @var mixed
+	 * @since 9.9.0
+	 **/
+	protected $entity_id;
+
+	/**
+	 * Date when the record was created.
+	 *
+	 * @var string
+	 * @since 9.9.0
+	 */
+	protected $date_created;
+
+	/**
+	 * Date when the record was deleted.
+	 *
+	 * @var string
+	 */
+	protected $date_deleted = null;
+
+	/**
+	 * Fulfillment items.
+	 *
+	 * @var array
+	 */
+	protected $items = array();
+
+	/**
+	 * Fulfillment constructor. Loads fulfillment data.
+	 *
+	 * @param mixed $data Fulfillment data.
+	 */
+	public function __construct( $data = '' ) {
+		parent::__construct( $data );
+
+		if ( $data instanceof Fulfillment ) {
+			$this->set_id( absint( $data->get_id() ) );
+		} elseif ( is_numeric( $data ) ) {
+			$this->set_id( absint( $data ) );
+		} elseif ( is_array( $data ) && isset( $data['id'] ) ) {
+			$this->set_id( absint( $data['id'] ) );
+		} elseif ( is_string( $data ) && ! empty( $data ) ) {
+			$this->set_id( absint( $data ) );
+		} elseif ( is_object( $data ) && isset( $data->id ) ) {
+			$this->set_id( absint( $data->id ) );
+		} else {
+			$this->set_object_read( true );
+		}
+
+		// Load the items array.
+		$this->data_store = new FulfillmentsDataStore();
+		if ( $this->get_id() > 0 ) {
+			$this->data_store->read( $this );
+		}
+	}
+
+	/**
+	 * Get the fulfillment ID.
+	 *
+	 * @return int Fulfillment ID.
+	 */
+	public function get_id() {
+		return $this->fulfillment_id;
+	}
+	/**
+	 * Set the fulfillment ID.
+	 *
+	 * @param int $id Fulfillment ID.
+	 */
+	public function set_id( $id ) {
path: plugins/woocommerce/src/Internal/Fulfillments/Fulfillment.php
user: login: prettyboymp
body: Can we specify the parameter types and return types in code where possible (meaning the type constraint is valid in PHP 7.4)?
created_at: 2025-04-22T19:51:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,200 @@
+<?php
+/**
+ * WooCommerce order fulfillments.
+ *
+ * The WooCommerce order fulfillments class gets contains fulfillment related properties and methods.
+ *
+ * @package WooCommerce\Classes
+ * @version 9.9.0
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\Fulfillments;
+
+use Automattic\WooCommerce\Internal\DataStores\Fulfillments\FulfillmentsDataStore;
+
+defined( 'ABSPATH' ) || exit;
+
+
+/**
+ * WC Order Fulfillment Class
+ *
+ * @since 9.9.0
+ */
+class Fulfillment extends \WC_Data {
+	/**
+	 * Fulfillment ID.
+	 *
+	 * @var int
+	 */
+	protected $fulfillment_id;
+
+	/**
+	 * Entity type. Will be referencing an object type on the system. For WC, this is WC_Order.
+	 *
+	 * @var string
+	 */
+	protected $entity_type;
+
+	/**
+	 * Entity ID. Can be string, int, or UUID according to the entity used with this record.
+	 *
+	 * @var mixed
+	 * @since 9.9.0
+	 **/
+	protected $entity_id;
+
+	/**
+	 * Date when the record was created.
+	 *
+	 * @var string
+	 * @since 9.9.0
+	 */
+	protected $date_created;
+
+	/**
+	 * Date when the record was deleted.
+	 *
+	 * @var string
+	 */
+	protected $date_deleted = null;
+
+	/**
+	 * Fulfillment items.
+	 *
+	 * @var array
+	 */
+	protected $items = array();
+
+	/**
+	 * Fulfillment constructor. Loads fulfillment data.
+	 *
+	 * @param mixed $data Fulfillment data.
+	 */
+	public function __construct( $data = '' ) {
+		parent::__construct( $data );
+
+		if ( $data instanceof Fulfillment ) {
+			$this->set_id( absint( $data->get_id() ) );
+		} elseif ( is_numeric( $data ) ) {
+			$this->set_id( absint( $data ) );
+		} elseif ( is_array( $data ) && isset( $data['id'] ) ) {
+			$this->set_id( absint( $data['id'] ) );
+		} elseif ( is_string( $data ) && ! empty( $data ) ) {
+			$this->set_id( absint( $data ) );
+		} elseif ( is_object( $data ) && isset( $data->id ) ) {
+			$this->set_id( absint( $data->id ) );
+		} else {
+			$this->set_object_read( true );
+		}
+
+		// Load the items array.
+		$this->data_store = new FulfillmentsDataStore();
path: plugins/woocommerce/src/Internal/Fulfillments/Fulfillment.php
user: login: prettyboymp
body: We'll want to be sure we use a shared datastore between entities so we don't end up with every entity pointing to a new datastore instance.
created_at: 2025-04-22T20:54:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,401 @@
+<?php
+/**
+ * Class FulfillmentsDataStore file.
+ *
+ * @package WooCommerce\DataStores
+ */
+
+namespace Automattic\WooCommerce\Internal\DataStores\Fulfillments;
+
+use Automattic\WooCommerce\Internal\Fulfillments\Fulfillment;
+use WP_Error;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * WC Order Item Product Data Store
+ *
+ * @version  9.9.0
+ */
+class FulfillmentsDataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface {
+
+	/**
+	 * Error object.
+	 *
+	 * @var WP_Error|null $error The error object.
+	 */
+	protected $error = null;
+
+	/**
+	 * Method to create a new fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to create.
+	 *
+	 * @return void
+	 */
+	public function create( &$data ) {
+		$this->clear_error();
+
+		// Validate the fulfillment data.
+		if ( ! $data->get_entity_type() ) {
+			$this->set_error( new WP_Error( 'invalid_entity_type', __( 'Invalid entity type.', 'woocommerce' ) ) );
path: plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStore.php
user: login: tpaksu
body: Yes, I wasn't sure about that either, when extending the data class, I saw that the docblocks show as return bool, return array, etc. but those return values weren't used in the data store implementation. Throwing an exception will be a better solution to stop the execution.
created_at: 2025-04-22T20:57:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,200 @@
+<?php
+/**
+ * WooCommerce order fulfillments.
+ *
+ * The WooCommerce order fulfillments class gets contains fulfillment related properties and methods.
+ *
+ * @package WooCommerce\Classes
+ * @version 9.9.0
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\Fulfillments;
+
+use Automattic\WooCommerce\Internal\DataStores\Fulfillments\FulfillmentsDataStore;
+
+defined( 'ABSPATH' ) || exit;
+
+
+/**
+ * WC Order Fulfillment Class
+ *
+ * @since 9.9.0
+ */
+class Fulfillment extends \WC_Data {
+	/**
+	 * Fulfillment ID.
+	 *
+	 * @var int
+	 */
+	protected $fulfillment_id;
+
+	/**
+	 * Entity type. Will be referencing an object type on the system. For WC, this is WC_Order.
+	 *
+	 * @var string
+	 */
+	protected $entity_type;
+
+	/**
+	 * Entity ID. Can be string, int, or UUID according to the entity used with this record.
+	 *
+	 * @var mixed
+	 * @since 9.9.0
+	 **/
+	protected $entity_id;
+
+	/**
+	 * Date when the record was created.
+	 *
+	 * @var string
+	 * @since 9.9.0
+	 */
+	protected $date_created;
+
+	/**
+	 * Date when the record was deleted.
+	 *
+	 * @var string
+	 */
+	protected $date_deleted = null;
+
+	/**
+	 * Fulfillment items.
+	 *
+	 * @var array
+	 */
+	protected $items = array();
+
+	/**
+	 * Fulfillment constructor. Loads fulfillment data.
+	 *
+	 * @param mixed $data Fulfillment data.
+	 */
+	public function __construct( $data = '' ) {
+		parent::__construct( $data );
+
+		if ( $data instanceof Fulfillment ) {
+			$this->set_id( absint( $data->get_id() ) );
+		} elseif ( is_numeric( $data ) ) {
+			$this->set_id( absint( $data ) );
+		} elseif ( is_array( $data ) && isset( $data['id'] ) ) {
+			$this->set_id( absint( $data['id'] ) );
+		} elseif ( is_string( $data ) && ! empty( $data ) ) {
+			$this->set_id( absint( $data ) );
+		} elseif ( is_object( $data ) && isset( $data->id ) ) {
+			$this->set_id( absint( $data->id ) );
+		} else {
+			$this->set_object_read( true );
+		}
+
+		// Load the items array.
+		$this->data_store = new FulfillmentsDataStore();
path: plugins/woocommerce/src/Internal/Fulfillments/Fulfillment.php
user: login: tpaksu
body: Got it. I'll use DI to get the instance, just saw it on the other data object code.
created_at: 2025-04-22T20:58:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,200 @@
+<?php
+/**
+ * WooCommerce order fulfillments.
+ *
+ * The WooCommerce order fulfillments class gets contains fulfillment related properties and methods.
+ *
+ * @package WooCommerce\Classes
+ * @version 9.9.0
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\Fulfillments;
+
+use Automattic\WooCommerce\Internal\DataStores\Fulfillments\FulfillmentsDataStore;
+
+defined( 'ABSPATH' ) || exit;
+
+
+/**
+ * WC Order Fulfillment Class
+ *
+ * @since 9.9.0
+ */
+class Fulfillment extends \WC_Data {
+	/**
+	 * Fulfillment ID.
+	 *
+	 * @var int
+	 */
+	protected $fulfillment_id;
+
+	/**
+	 * Entity type. Will be referencing an object type on the system. For WC, this is WC_Order.
+	 *
+	 * @var string
+	 */
+	protected $entity_type;
+
+	/**
+	 * Entity ID. Can be string, int, or UUID according to the entity used with this record.
+	 *
+	 * @var mixed
+	 * @since 9.9.0
+	 **/
+	protected $entity_id;
+
+	/**
+	 * Date when the record was created.
+	 *
+	 * @var string
+	 * @since 9.9.0
+	 */
+	protected $date_created;
+
+	/**
+	 * Date when the record was deleted.
+	 *
+	 * @var string
+	 */
+	protected $date_deleted = null;
+
+	/**
+	 * Fulfillment items.
+	 *
+	 * @var array
+	 */
+	protected $items = array();
+
+	/**
+	 * Fulfillment constructor. Loads fulfillment data.
+	 *
+	 * @param mixed $data Fulfillment data.
+	 */
+	public function __construct( $data = '' ) {
+		parent::__construct( $data );
+
+		if ( $data instanceof Fulfillment ) {
+			$this->set_id( absint( $data->get_id() ) );
+		} elseif ( is_numeric( $data ) ) {
+			$this->set_id( absint( $data ) );
+		} elseif ( is_array( $data ) && isset( $data['id'] ) ) {
+			$this->set_id( absint( $data['id'] ) );
+		} elseif ( is_string( $data ) && ! empty( $data ) ) {
+			$this->set_id( absint( $data ) );
+		} elseif ( is_object( $data ) && isset( $data->id ) ) {
+			$this->set_id( absint( $data->id ) );
+		} else {
+			$this->set_object_read( true );
+		}
+
+		// Load the items array.
+		$this->data_store = new FulfillmentsDataStore();
path: plugins/woocommerce/src/Internal/Fulfillments/Fulfillment.php
user: login: tpaksu
body: Addressed in [6f4db67](https://github.com/woocommerce/woocommerce/pull/57429/commits/6f4db670a3eb2edff79903047b3713c133ec368d).
created_at: 2025-04-22T21:59:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,401 @@
+<?php
+/**
+ * Class FulfillmentsDataStore file.
+ *
+ * @package WooCommerce\DataStores
+ */
+
+namespace Automattic\WooCommerce\Internal\DataStores\Fulfillments;
+
+use Automattic\WooCommerce\Internal\Fulfillments\Fulfillment;
+use WP_Error;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * WC Order Item Product Data Store
+ *
+ * @version  9.9.0
+ */
+class FulfillmentsDataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface {
+
+	/**
+	 * Error object.
+	 *
+	 * @var WP_Error|null $error The error object.
+	 */
+	protected $error = null;
+
+	/**
+	 * Method to create a new fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to create.
+	 *
+	 * @return void
+	 */
+	public function create( &$data ) {
+		$this->clear_error();
+
+		// Validate the fulfillment data.
+		if ( ! $data->get_entity_type() ) {
+			$this->set_error( new WP_Error( 'invalid_entity_type', __( 'Invalid entity type.', 'woocommerce' ) ) );
path: plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStore.php
user: login: tpaksu
body: Addressed in [0e98a2c](https://github.com/woocommerce/woocommerce/pull/57429/commits/0e98a2cd980f305f0321bdef4877c52ad559ba8e).
created_at: 2025-04-22T22:00:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,200 @@
+<?php
+/**
+ * WooCommerce order fulfillments.
+ *
+ * The WooCommerce order fulfillments class gets contains fulfillment related properties and methods.
+ *
+ * @package WooCommerce\Classes
+ * @version 9.9.0
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\Fulfillments;
+
+use Automattic\WooCommerce\Internal\DataStores\Fulfillments\FulfillmentsDataStore;
+
+defined( 'ABSPATH' ) || exit;
+
+
+/**
+ * WC Order Fulfillment Class
+ *
+ * @since 9.9.0
+ */
+class Fulfillment extends \WC_Data {
+	/**
+	 * Fulfillment ID.
+	 *
+	 * @var int
+	 */
+	protected $fulfillment_id;
+
+	/**
+	 * Entity type. Will be referencing an object type on the system. For WC, this is WC_Order.
+	 *
+	 * @var string
+	 */
+	protected $entity_type;
+
+	/**
+	 * Entity ID. Can be string, int, or UUID according to the entity used with this record.
+	 *
+	 * @var mixed
+	 * @since 9.9.0
+	 **/
+	protected $entity_id;
+
+	/**
+	 * Date when the record was created.
+	 *
+	 * @var string
+	 * @since 9.9.0
+	 */
+	protected $date_created;
+
+	/**
+	 * Date when the record was deleted.
+	 *
+	 * @var string
+	 */
+	protected $date_deleted = null;
+
+	/**
+	 * Fulfillment items.
+	 *
+	 * @var array
+	 */
+	protected $items = array();
+
+	/**
+	 * Fulfillment constructor. Loads fulfillment data.
+	 *
+	 * @param mixed $data Fulfillment data.
+	 */
+	public function __construct( $data = '' ) {
+		parent::__construct( $data );
+
+		if ( $data instanceof Fulfillment ) {
+			$this->set_id( absint( $data->get_id() ) );
+		} elseif ( is_numeric( $data ) ) {
+			$this->set_id( absint( $data ) );
+		} elseif ( is_array( $data ) && isset( $data['id'] ) ) {
+			$this->set_id( absint( $data['id'] ) );
+		} elseif ( is_string( $data ) && ! empty( $data ) ) {
+			$this->set_id( absint( $data ) );
+		} elseif ( is_object( $data ) && isset( $data->id ) ) {
+			$this->set_id( absint( $data->id ) );
+		} else {
+			$this->set_object_read( true );
+		}
+
+		// Load the items array.
+		$this->data_store = new FulfillmentsDataStore();
+		if ( $this->get_id() > 0 ) {
+			$this->data_store->read( $this );
+		}
+	}
+
+	/**
+	 * Get the fulfillment ID.
+	 *
+	 * @return int Fulfillment ID.
+	 */
+	public function get_id() {
+		return $this->fulfillment_id;
+	}
+	/**
+	 * Set the fulfillment ID.
+	 *
+	 * @param int $id Fulfillment ID.
+	 */
+	public function set_id( $id ) {
path: plugins/woocommerce/src/Internal/Fulfillments/Fulfillment.php
user: login: tpaksu
body: Addressed in [ec7c946](https://github.com/woocommerce/woocommerce/pull/57429/commits/ec7c946bd84cb1abcca349682de6e46361971fa0).
created_at: 2025-04-22T22:25:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+<?php
+/**
+ * Class FulfillmentsDataStore file.
+ *
+ * @package WooCommerce\DataStores
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\DataStores\Fulfillments;
+
+use Automattic\WooCommerce\Internal\Fulfillments\Fulfillment;
+use WC_Meta_Data;
+use WP_Error;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * WC Order Item Product Data Store
+ *
+ * @version  9.9.0
+ */
+class FulfillmentsDataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface {
+
+	/**
+	 * Method to create a new fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to create.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data is invalid.
+	 * @throws \Exception If the fulfillment can't be created.
+	 */
+	public function create( &$data ): void {
+		// Validate the fulfillment data.
+		if ( ! $data->get_entity_type() ) {
+			throw new \Exception( esc_html__( 'Invalid entity type.', 'woocommerce' ) );
+		}
+		if ( ! $data->get_entity_id() ) {
+			throw new \Exception( esc_html__( 'Invalid entity ID.', 'woocommerce' ) );
+		}
+		if ( empty( $data->get_items() ) ) {
+			throw new \Exception( esc_html__( 'The fulfillment should contain at least one item.', 'woocommerce' ) );
+		}
+		foreach ( $data->get_items() as $item ) {
+			if ( ! isset( $item['item_id'] ) || ! isset( $item['qty'] ) ) {
+				throw new \Exception( esc_html__( 'Invalid item.', 'woocommerce' ) );
+			}
+		}
+
+		// Set fulfillment properties.
+		$data->set_date_updated( new \DateTime( current_time( 'mysql' ) ) );
+		$data->set_date_deleted( null );
path: plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStore.php
user: login: prettyboymp
body: Since `Fulfillment::date_deleted` defaults to `null`, can we remove this?  That way if there is a valid reason for a new Fulfillment to be created in a deleted state, e.g. imports/migrations, the client doesn't have to save twice. 
created_at: 2025-04-24T13:46:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+<?php
+/**
+ * Class FulfillmentsDataStore file.
+ *
+ * @package WooCommerce\DataStores
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\DataStores\Fulfillments;
+
+use Automattic\WooCommerce\Internal\Fulfillments\Fulfillment;
+use WC_Meta_Data;
+use WP_Error;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * WC Order Item Product Data Store
+ *
+ * @version  9.9.0
+ */
+class FulfillmentsDataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface {
+
+	/**
+	 * Method to create a new fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to create.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data is invalid.
+	 * @throws \Exception If the fulfillment can't be created.
+	 */
+	public function create( &$data ): void {
+		// Validate the fulfillment data.
+		if ( ! $data->get_entity_type() ) {
+			throw new \Exception( esc_html__( 'Invalid entity type.', 'woocommerce' ) );
+		}
+		if ( ! $data->get_entity_id() ) {
+			throw new \Exception( esc_html__( 'Invalid entity ID.', 'woocommerce' ) );
+		}
+		if ( empty( $data->get_items() ) ) {
+			throw new \Exception( esc_html__( 'The fulfillment should contain at least one item.', 'woocommerce' ) );
+		}
+		foreach ( $data->get_items() as $item ) {
+			if ( ! isset( $item['item_id'] ) || ! isset( $item['qty'] ) ) {
+				throw new \Exception( esc_html__( 'Invalid item.', 'woocommerce' ) );
+			}
+		}
+
+		// Set fulfillment properties.
+		$data->set_date_updated( new \DateTime( current_time( 'mysql' ) ) );
+		$data->set_date_deleted( null );
+
+		// Save the fulfillment to the database.
+		global $wpdb;
+		$data_id = $wpdb->insert(
path: plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStore.php
user: login: prettyboymp
body: Setting the result of the insert to `$data_id` may be confusing since $wpdb returns the number of rows inserted here.
created_at: 2025-04-24T13:49:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+<?php
+/**
+ * Class FulfillmentsDataStore file.
+ *
+ * @package WooCommerce\DataStores
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\DataStores\Fulfillments;
+
+use Automattic\WooCommerce\Internal\Fulfillments\Fulfillment;
+use WC_Meta_Data;
+use WP_Error;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * WC Order Item Product Data Store
+ *
+ * @version  9.9.0
+ */
+class FulfillmentsDataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface {
+
+	/**
+	 * Method to create a new fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to create.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data is invalid.
+	 * @throws \Exception If the fulfillment can't be created.
+	 */
+	public function create( &$data ): void {
+		// Validate the fulfillment data.
+		if ( ! $data->get_entity_type() ) {
+			throw new \Exception( esc_html__( 'Invalid entity type.', 'woocommerce' ) );
+		}
+		if ( ! $data->get_entity_id() ) {
+			throw new \Exception( esc_html__( 'Invalid entity ID.', 'woocommerce' ) );
+		}
+		if ( empty( $data->get_items() ) ) {
+			throw new \Exception( esc_html__( 'The fulfillment should contain at least one item.', 'woocommerce' ) );
+		}
+		foreach ( $data->get_items() as $item ) {
+			if ( ! isset( $item['item_id'] ) || ! isset( $item['qty'] ) ) {
+				throw new \Exception( esc_html__( 'Invalid item.', 'woocommerce' ) );
+			}
+		}
+
+		// Set fulfillment properties.
+		$data->set_date_updated( new \DateTime( current_time( 'mysql' ) ) );
+		$data->set_date_deleted( null );
+
+		// Save the fulfillment to the database.
+		global $wpdb;
+		$data_id = $wpdb->insert(
+			$wpdb->prefix . 'wc_order_fulfillments',
+			array(
+				'entity_type'  => $data->get_entity_type(),
+				'entity_id'    => $data->get_entity_id(),
+				'date_updated' => $data->get_date_updated()->format( 'Y-m-d H:i:s' ),
+				'date_deleted' => $data->get_date_deleted() ? $data->get_date_deleted()->format( 'Y-m-d H:i:s' ) : null,
+			),
+			array(
+				'%s',
+				'%d',
+				'%s',
+				'%s',
+			)
+		);
+
+		// Check for errors.
+		if ( false === $data_id ) {
+			throw new \Exception( esc_html__( 'Failed to insert fulfillment.', 'woocommerce' ) );
+		}
+
+		// Set the ID of the fulfillment object.
+		$data_id = $wpdb->insert_id;
+
+		$data->set_id( $data_id );
+
+		// Save the metadata for the fulfillment to the database.
+		$data->save_meta_data();
+
+		// Apply changes let's the object know that the current object reflects the database and no "changes" exist between the two.
+		$data->apply_changes();
+		$data->set_object_read( true );
+	}
+
+	/**
+	 * Method to read a fulfillment from the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to read.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data can't be read.
+	 */
+	public function read( &$data ): void {
+		// Read the fulfillment from the database.
+		global $wpdb;
+
+		$data_id          = $data->get_id();
+		$fulfillment_data = $wpdb->get_row(
path: plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStore.php
user: login: prettyboymp
body: This is okay for an initial pass, but we'll probably want to make sure we implement a `read_multiple()` or similar interface for loading multiple Fulfillments at once so we don't have to run a query for each one being displayed/returned.
created_at: 2025-04-24T13:59:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+<?php
+/**
+ * Class FulfillmentsDataStore file.
+ *
+ * @package WooCommerce\DataStores
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\DataStores\Fulfillments;
+
+use Automattic\WooCommerce\Internal\Fulfillments\Fulfillment;
+use WC_Meta_Data;
+use WP_Error;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * WC Order Item Product Data Store
+ *
+ * @version  9.9.0
+ */
+class FulfillmentsDataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface {
+
+	/**
+	 * Method to create a new fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to create.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data is invalid.
+	 * @throws \Exception If the fulfillment can't be created.
+	 */
+	public function create( &$data ): void {
+		// Validate the fulfillment data.
+		if ( ! $data->get_entity_type() ) {
+			throw new \Exception( esc_html__( 'Invalid entity type.', 'woocommerce' ) );
+		}
+		if ( ! $data->get_entity_id() ) {
+			throw new \Exception( esc_html__( 'Invalid entity ID.', 'woocommerce' ) );
+		}
+		if ( empty( $data->get_items() ) ) {
+			throw new \Exception( esc_html__( 'The fulfillment should contain at least one item.', 'woocommerce' ) );
+		}
+		foreach ( $data->get_items() as $item ) {
+			if ( ! isset( $item['item_id'] ) || ! isset( $item['qty'] ) ) {
+				throw new \Exception( esc_html__( 'Invalid item.', 'woocommerce' ) );
+			}
+		}
+
+		// Set fulfillment properties.
+		$data->set_date_updated( new \DateTime( current_time( 'mysql' ) ) );
+		$data->set_date_deleted( null );
+
+		// Save the fulfillment to the database.
+		global $wpdb;
+		$data_id = $wpdb->insert(
+			$wpdb->prefix . 'wc_order_fulfillments',
+			array(
+				'entity_type'  => $data->get_entity_type(),
+				'entity_id'    => $data->get_entity_id(),
+				'date_updated' => $data->get_date_updated()->format( 'Y-m-d H:i:s' ),
+				'date_deleted' => $data->get_date_deleted() ? $data->get_date_deleted()->format( 'Y-m-d H:i:s' ) : null,
+			),
+			array(
+				'%s',
+				'%d',
+				'%s',
+				'%s',
+			)
+		);
+
+		// Check for errors.
+		if ( false === $data_id ) {
+			throw new \Exception( esc_html__( 'Failed to insert fulfillment.', 'woocommerce' ) );
+		}
+
+		// Set the ID of the fulfillment object.
+		$data_id = $wpdb->insert_id;
+
+		$data->set_id( $data_id );
+
+		// Save the metadata for the fulfillment to the database.
+		$data->save_meta_data();
+
+		// Apply changes let's the object know that the current object reflects the database and no "changes" exist between the two.
+		$data->apply_changes();
+		$data->set_object_read( true );
+	}
+
+	/**
+	 * Method to read a fulfillment from the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to read.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data can't be read.
+	 */
+	public function read( &$data ): void {
+		// Read the fulfillment from the database.
+		global $wpdb;
+
+		$data_id          = $data->get_id();
+		$fulfillment_data = $wpdb->get_row(
+			$wpdb->prepare(
+				"SELECT * FROM {$wpdb->prefix}wc_order_fulfillments WHERE fulfillment_id = %d",
+				$data_id
+			),
+			ARRAY_A
+		);
+
+		if ( empty( $fulfillment_data ) ) {
+			throw new \Exception( esc_html__( 'Fulfillment not found.', 'woocommerce' ) );
+		}
+
+		$data->set_props(
+			array(
+				'fulfillment_id' => $fulfillment_data['fulfillment_id'],
+				'entity_type'    => $fulfillment_data['entity_type'],
+				'entity_id'      => $fulfillment_data['entity_id'],
+				'date_updated'   => new \DateTime( $fulfillment_data['date_updated'] ),
+				'date_deleted'   => $fulfillment_data['date_deleted'] ? new \DateTime( $fulfillment_data['date_deleted'] ) : null,
+			)
+		);
+
+		$data->read_meta_data( true );
+
+		$data->set_object_read( true );
+	}
+
+	/**
+	 * Method to update an existing fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to update.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment can't be updated.
+	 */
+	public function update( &$data ): void {
+		// Update the fulfillment in the database.
+		global $wpdb;
+
+		$data_id = $data->get_id();
+		$wpdb->update(
+			$wpdb->prefix . 'wc_order_fulfillments',
+			array(
+				'entity_type'  => $data->get_entity_type(),
+				'entity_id'    => $data->get_entity_id(),
+				'date_updated' => $data->get_date_updated()->format( 'Y-m-d H:i:s' ),
+				'date_deleted' => $data->get_date_deleted() ? $data->get_date_deleted()->format( 'Y-m-d H:i:s' ) : null,
+			),
+			array( 'fulfillment_id' => $data_id ),
+			array(
+				'%s',
+				'%d',
+				'%s',
+				'%s',
+			),
+			array( '%d' )
+		);
+
+		// Check for errors.
+		if ( $wpdb->last_error ) {
+			throw new \Exception( esc_html__( 'Failed to update fulfillment.', 'woocommerce' ) );
+		}
+
+		// Update the metadata for the fulfillment.
+		$data->save_meta_data();
+		$data->apply_changes();
+
+		$data->set_object_read( true );
+	}
+
+	/**
+	 * Method to delete a fulfillment from the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to delete.
+	 * @param array       $args Optional arguments to pass to the delete method.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment can't be deleted.
+	 */
+	public function delete( &$data, $args = array() ): void {
+		// Soft Delete the fulfillment from the database.
+		global $wpdb;
+
+		$data_id       = $data->get_id();
+		$deletion_time = current_time( 'mysql' );
+		$wpdb->update(
+			$wpdb->prefix . 'wc_order_fulfillments',
+			array(
+				'date_deleted' => $deletion_time,
+			),
+			array( 'fulfillment_id' => $data_id ),
+			array(
+				'%s',
+			),
+			array( '%d' )
+		);
+
+		// Check for errors.
+		if ( $wpdb->last_error ) {
+			throw new \Exception( esc_html__( 'Failed to delete fulfillment.', 'woocommerce' ) );
+		}
+
+		// Delete the metadata for the fulfillment.
+		$wpdb->delete(
path: plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStore.php
user: login: prettyboymp
body: In #57428 the meta table has a `date_deleted` column.  Is there a reason to hard delete meta if we re only soft deleting the fulfillment?  
created_at: 2025-04-24T14:55:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,202 @@
+<?php
+/**
+ * WooCommerce order fulfillments.
+ *
+ * The WooCommerce order fulfillments class gets contains fulfillment related properties and methods.
+ *
+ * @package WooCommerce\Classes
+ * @version 9.9.0
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\Fulfillments;
+
+use Automattic\WooCommerce\Internal\DataStores\Fulfillments\FulfillmentsDataStore;
+
+defined( 'ABSPATH' ) || exit;
+
+
+/**
+ * WC Order Fulfillment Class
+ *
+ * @since 9.9.0
+ */
+class Fulfillment extends \WC_Data {
+	/**
+	 * Fulfillment ID.
+	 *
+	 * @var int
+	 */
+	protected int $fulfillment_id = 0;
path: plugins/woocommerce/src/Internal/Fulfillments/Fulfillment.php
user: login: prettyboymp
body: If we're extending `\WC_Data` all of the attributes need to be stored in `$data` instead of object properties. It is what allows tracking of changes while a WC_Data object is in a changed state prior to saving back to the DB.  The blended approach where some of the properties are duplicated, like the fulfillment_id and items, it will likely cause bugs down the line if we don't have confidence in which is the canonical source for the value.
created_at: 2025-04-24T15:21:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+<?php
+/**
+ * Class FulfillmentsDataStore file.
+ *
+ * @package WooCommerce\DataStores
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\DataStores\Fulfillments;
+
+use Automattic\WooCommerce\Internal\Fulfillments\Fulfillment;
+use WC_Meta_Data;
+use WP_Error;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * WC Order Item Product Data Store
+ *
+ * @version  9.9.0
+ */
+class FulfillmentsDataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface {
+
+	/**
+	 * Method to create a new fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to create.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data is invalid.
+	 * @throws \Exception If the fulfillment can't be created.
+	 */
+	public function create( &$data ): void {
+		// Validate the fulfillment data.
+		if ( ! $data->get_entity_type() ) {
+			throw new \Exception( esc_html__( 'Invalid entity type.', 'woocommerce' ) );
+		}
+		if ( ! $data->get_entity_id() ) {
+			throw new \Exception( esc_html__( 'Invalid entity ID.', 'woocommerce' ) );
+		}
+		if ( empty( $data->get_items() ) ) {
+			throw new \Exception( esc_html__( 'The fulfillment should contain at least one item.', 'woocommerce' ) );
+		}
+		foreach ( $data->get_items() as $item ) {
+			if ( ! isset( $item['item_id'] ) || ! isset( $item['qty'] ) ) {
+				throw new \Exception( esc_html__( 'Invalid item.', 'woocommerce' ) );
+			}
+		}
+
+		// Set fulfillment properties.
+		$data->set_date_updated( new \DateTime( current_time( 'mysql' ) ) );
+		$data->set_date_deleted( null );
+
+		// Save the fulfillment to the database.
+		global $wpdb;
+		$data_id = $wpdb->insert(
+			$wpdb->prefix . 'wc_order_fulfillments',
+			array(
+				'entity_type'  => $data->get_entity_type(),
+				'entity_id'    => $data->get_entity_id(),
+				'date_updated' => $data->get_date_updated()->format( 'Y-m-d H:i:s' ),
+				'date_deleted' => $data->get_date_deleted() ? $data->get_date_deleted()->format( 'Y-m-d H:i:s' ) : null,
+			),
+			array(
+				'%s',
+				'%d',
+				'%s',
+				'%s',
+			)
+		);
+
+		// Check for errors.
+		if ( false === $data_id ) {
+			throw new \Exception( esc_html__( 'Failed to insert fulfillment.', 'woocommerce' ) );
+		}
+
+		// Set the ID of the fulfillment object.
+		$data_id = $wpdb->insert_id;
+
+		$data->set_id( $data_id );
+
+		// Save the metadata for the fulfillment to the database.
+		$data->save_meta_data();
+
+		// Apply changes let's the object know that the current object reflects the database and no "changes" exist between the two.
+		$data->apply_changes();
+		$data->set_object_read( true );
+	}
+
+	/**
+	 * Method to read a fulfillment from the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to read.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data can't be read.
+	 */
+	public function read( &$data ): void {
+		// Read the fulfillment from the database.
+		global $wpdb;
+
+		$data_id          = $data->get_id();
+		$fulfillment_data = $wpdb->get_row(
+			$wpdb->prepare(
+				"SELECT * FROM {$wpdb->prefix}wc_order_fulfillments WHERE fulfillment_id = %d",
+				$data_id
+			),
+			ARRAY_A
+		);
+
+		if ( empty( $fulfillment_data ) ) {
+			throw new \Exception( esc_html__( 'Fulfillment not found.', 'woocommerce' ) );
+		}
+
+		$data->set_props(
+			array(
+				'fulfillment_id' => $fulfillment_data['fulfillment_id'],
+				'entity_type'    => $fulfillment_data['entity_type'],
+				'entity_id'      => $fulfillment_data['entity_id'],
+				'date_updated'   => new \DateTime( $fulfillment_data['date_updated'] ),
+				'date_deleted'   => $fulfillment_data['date_deleted'] ? new \DateTime( $fulfillment_data['date_deleted'] ) : null,
+			)
+		);
+
+		$data->read_meta_data( true );
+
+		$data->set_object_read( true );
+	}
+
+	/**
+	 * Method to update an existing fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to update.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment can't be updated.
+	 */
+	public function update( &$data ): void {
+		// Update the fulfillment in the database.
+		global $wpdb;
+
+		$data_id = $data->get_id();
+		$wpdb->update(
+			$wpdb->prefix . 'wc_order_fulfillments',
+			array(
+				'entity_type'  => $data->get_entity_type(),
+				'entity_id'    => $data->get_entity_id(),
+				'date_updated' => $data->get_date_updated()->format( 'Y-m-d H:i:s' ),
+				'date_deleted' => $data->get_date_deleted() ? $data->get_date_deleted()->format( 'Y-m-d H:i:s' ) : null,
+			),
+			array( 'fulfillment_id' => $data_id ),
+			array(
+				'%s',
+				'%d',
+				'%s',
+				'%s',
+			),
+			array( '%d' )
+		);
+
+		// Check for errors.
+		if ( $wpdb->last_error ) {
+			throw new \Exception( esc_html__( 'Failed to update fulfillment.', 'woocommerce' ) );
+		}
+
+		// Update the metadata for the fulfillment.
+		$data->save_meta_data();
+		$data->apply_changes();
+
+		$data->set_object_read( true );
+	}
+
+	/**
+	 * Method to delete a fulfillment from the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to delete.
+	 * @param array       $args Optional arguments to pass to the delete method.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment can't be deleted.
+	 */
+	public function delete( &$data, $args = array() ): void {
+		// Soft Delete the fulfillment from the database.
+		global $wpdb;
+
+		$data_id       = $data->get_id();
+		$deletion_time = current_time( 'mysql' );
+		$wpdb->update(
+			$wpdb->prefix . 'wc_order_fulfillments',
+			array(
+				'date_deleted' => $deletion_time,
+			),
+			array( 'fulfillment_id' => $data_id ),
+			array(
+				'%s',
+			),
+			array( '%d' )
+		);
+
+		// Check for errors.
+		if ( $wpdb->last_error ) {
+			throw new \Exception( esc_html__( 'Failed to delete fulfillment.', 'woocommerce' ) );
+		}
+
+		// Delete the metadata for the fulfillment.
+		$wpdb->delete(
path: plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStore.php
user: login: tpaksu
body: We will want to keep the history of the order fulfillments with soft deleting records, but I think we may not want to keep the old metas. By keeping the deleted fulfillments on the table, we can see the history of order fulfillments by looking at the timeline, and their last state of their metadata, but I believe we will only will need the last state of the fulfillment meta. But if you have some experience about why keeping history of metas is important, I'll appreciate if you share it, so I can add a date_deleted column to the meta table. 
created_at: 2025-04-24T19:48:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+<?php
+/**
+ * Class FulfillmentsDataStore file.
+ *
+ * @package WooCommerce\DataStores
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\DataStores\Fulfillments;
+
+use Automattic\WooCommerce\Internal\Fulfillments\Fulfillment;
+use WC_Meta_Data;
+use WP_Error;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * WC Order Item Product Data Store
+ *
+ * @version  9.9.0
+ */
+class FulfillmentsDataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface {
+
+	/**
+	 * Method to create a new fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to create.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data is invalid.
+	 * @throws \Exception If the fulfillment can't be created.
+	 */
+	public function create( &$data ): void {
+		// Validate the fulfillment data.
+		if ( ! $data->get_entity_type() ) {
+			throw new \Exception( esc_html__( 'Invalid entity type.', 'woocommerce' ) );
+		}
+		if ( ! $data->get_entity_id() ) {
+			throw new \Exception( esc_html__( 'Invalid entity ID.', 'woocommerce' ) );
+		}
+		if ( empty( $data->get_items() ) ) {
+			throw new \Exception( esc_html__( 'The fulfillment should contain at least one item.', 'woocommerce' ) );
+		}
+		foreach ( $data->get_items() as $item ) {
+			if ( ! isset( $item['item_id'] ) || ! isset( $item['qty'] ) ) {
+				throw new \Exception( esc_html__( 'Invalid item.', 'woocommerce' ) );
+			}
+		}
+
+		// Set fulfillment properties.
+		$data->set_date_updated( new \DateTime( current_time( 'mysql' ) ) );
+		$data->set_date_deleted( null );
+
+		// Save the fulfillment to the database.
+		global $wpdb;
+		$data_id = $wpdb->insert(
+			$wpdb->prefix . 'wc_order_fulfillments',
+			array(
+				'entity_type'  => $data->get_entity_type(),
+				'entity_id'    => $data->get_entity_id(),
+				'date_updated' => $data->get_date_updated()->format( 'Y-m-d H:i:s' ),
+				'date_deleted' => $data->get_date_deleted() ? $data->get_date_deleted()->format( 'Y-m-d H:i:s' ) : null,
+			),
+			array(
+				'%s',
+				'%d',
+				'%s',
+				'%s',
+			)
+		);
+
+		// Check for errors.
+		if ( false === $data_id ) {
+			throw new \Exception( esc_html__( 'Failed to insert fulfillment.', 'woocommerce' ) );
+		}
+
+		// Set the ID of the fulfillment object.
+		$data_id = $wpdb->insert_id;
+
+		$data->set_id( $data_id );
+
+		// Save the metadata for the fulfillment to the database.
+		$data->save_meta_data();
+
+		// Apply changes let's the object know that the current object reflects the database and no "changes" exist between the two.
+		$data->apply_changes();
+		$data->set_object_read( true );
+	}
+
+	/**
+	 * Method to read a fulfillment from the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to read.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data can't be read.
+	 */
+	public function read( &$data ): void {
+		// Read the fulfillment from the database.
+		global $wpdb;
+
+		$data_id          = $data->get_id();
+		$fulfillment_data = $wpdb->get_row(
path: plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStore.php
user: login: tpaksu
body: Yes, I realized it while building the REST API, as the current state of the data store doesn't allow me to query the DB with entity type and id. I will add a new interface that will contain the REST method <-> database bridging functions and extend the data store with their implementations.
created_at: 2025-04-24T19:50:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+<?php
+/**
+ * Class FulfillmentsDataStore file.
+ *
+ * @package WooCommerce\DataStores
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\DataStores\Fulfillments;
+
+use Automattic\WooCommerce\Internal\Fulfillments\Fulfillment;
+use WC_Meta_Data;
+use WP_Error;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * WC Order Item Product Data Store
+ *
+ * @version  9.9.0
+ */
+class FulfillmentsDataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface {
+
+	/**
+	 * Method to create a new fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to create.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data is invalid.
+	 * @throws \Exception If the fulfillment can't be created.
+	 */
+	public function create( &$data ): void {
+		// Validate the fulfillment data.
+		if ( ! $data->get_entity_type() ) {
+			throw new \Exception( esc_html__( 'Invalid entity type.', 'woocommerce' ) );
+		}
+		if ( ! $data->get_entity_id() ) {
+			throw new \Exception( esc_html__( 'Invalid entity ID.', 'woocommerce' ) );
+		}
+		if ( empty( $data->get_items() ) ) {
+			throw new \Exception( esc_html__( 'The fulfillment should contain at least one item.', 'woocommerce' ) );
+		}
+		foreach ( $data->get_items() as $item ) {
+			if ( ! isset( $item['item_id'] ) || ! isset( $item['qty'] ) ) {
+				throw new \Exception( esc_html__( 'Invalid item.', 'woocommerce' ) );
+			}
+		}
+
+		// Set fulfillment properties.
+		$data->set_date_updated( new \DateTime( current_time( 'mysql' ) ) );
+		$data->set_date_deleted( null );
+
+		// Save the fulfillment to the database.
+		global $wpdb;
+		$data_id = $wpdb->insert(
+			$wpdb->prefix . 'wc_order_fulfillments',
+			array(
+				'entity_type'  => $data->get_entity_type(),
+				'entity_id'    => $data->get_entity_id(),
+				'date_updated' => $data->get_date_updated()->format( 'Y-m-d H:i:s' ),
+				'date_deleted' => $data->get_date_deleted() ? $data->get_date_deleted()->format( 'Y-m-d H:i:s' ) : null,
+			),
+			array(
+				'%s',
+				'%d',
+				'%s',
+				'%s',
+			)
+		);
+
+		// Check for errors.
+		if ( false === $data_id ) {
+			throw new \Exception( esc_html__( 'Failed to insert fulfillment.', 'woocommerce' ) );
+		}
+
+		// Set the ID of the fulfillment object.
+		$data_id = $wpdb->insert_id;
+
+		$data->set_id( $data_id );
+
+		// Save the metadata for the fulfillment to the database.
+		$data->save_meta_data();
+
+		// Apply changes let's the object know that the current object reflects the database and no "changes" exist between the two.
+		$data->apply_changes();
+		$data->set_object_read( true );
+	}
+
+	/**
+	 * Method to read a fulfillment from the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to read.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data can't be read.
+	 */
+	public function read( &$data ): void {
+		// Read the fulfillment from the database.
+		global $wpdb;
+
+		$data_id          = $data->get_id();
+		$fulfillment_data = $wpdb->get_row(
+			$wpdb->prepare(
+				"SELECT * FROM {$wpdb->prefix}wc_order_fulfillments WHERE fulfillment_id = %d",
+				$data_id
+			),
+			ARRAY_A
+		);
+
+		if ( empty( $fulfillment_data ) ) {
+			throw new \Exception( esc_html__( 'Fulfillment not found.', 'woocommerce' ) );
+		}
+
+		$data->set_props(
+			array(
+				'fulfillment_id' => $fulfillment_data['fulfillment_id'],
+				'entity_type'    => $fulfillment_data['entity_type'],
+				'entity_id'      => $fulfillment_data['entity_id'],
+				'date_updated'   => new \DateTime( $fulfillment_data['date_updated'] ),
+				'date_deleted'   => $fulfillment_data['date_deleted'] ? new \DateTime( $fulfillment_data['date_deleted'] ) : null,
+			)
+		);
+
+		$data->read_meta_data( true );
+
+		$data->set_object_read( true );
+	}
+
+	/**
+	 * Method to update an existing fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to update.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment can't be updated.
+	 */
+	public function update( &$data ): void {
+		// Update the fulfillment in the database.
+		global $wpdb;
+
+		$data_id = $data->get_id();
+		$wpdb->update(
+			$wpdb->prefix . 'wc_order_fulfillments',
+			array(
+				'entity_type'  => $data->get_entity_type(),
+				'entity_id'    => $data->get_entity_id(),
+				'date_updated' => $data->get_date_updated()->format( 'Y-m-d H:i:s' ),
+				'date_deleted' => $data->get_date_deleted() ? $data->get_date_deleted()->format( 'Y-m-d H:i:s' ) : null,
+			),
+			array( 'fulfillment_id' => $data_id ),
+			array(
+				'%s',
+				'%d',
+				'%s',
+				'%s',
+			),
+			array( '%d' )
+		);
+
+		// Check for errors.
+		if ( $wpdb->last_error ) {
+			throw new \Exception( esc_html__( 'Failed to update fulfillment.', 'woocommerce' ) );
+		}
+
+		// Update the metadata for the fulfillment.
+		$data->save_meta_data();
+		$data->apply_changes();
+
+		$data->set_object_read( true );
+	}
+
+	/**
+	 * Method to delete a fulfillment from the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to delete.
+	 * @param array       $args Optional arguments to pass to the delete method.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment can't be deleted.
+	 */
+	public function delete( &$data, $args = array() ): void {
+		// Soft Delete the fulfillment from the database.
+		global $wpdb;
+
+		$data_id       = $data->get_id();
+		$deletion_time = current_time( 'mysql' );
+		$wpdb->update(
+			$wpdb->prefix . 'wc_order_fulfillments',
+			array(
+				'date_deleted' => $deletion_time,
+			),
+			array( 'fulfillment_id' => $data_id ),
+			array(
+				'%s',
+			),
+			array( '%d' )
+		);
+
+		// Check for errors.
+		if ( $wpdb->last_error ) {
+			throw new \Exception( esc_html__( 'Failed to delete fulfillment.', 'woocommerce' ) );
+		}
+
+		// Delete the metadata for the fulfillment.
+		$wpdb->delete(
path: plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStore.php
user: login: tpaksu
body: Okay, maybe we should not allow users to delete metadata at all. We can only allow creating and updating a metadata, so we will be keeping the last state of the metadata when the fulfillment is deleted. How does it sound?
created_at: 2025-04-25T00:21:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,202 @@
+<?php
+/**
+ * WooCommerce order fulfillments.
+ *
+ * The WooCommerce order fulfillments class gets contains fulfillment related properties and methods.
+ *
+ * @package WooCommerce\Classes
+ * @version 9.9.0
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\Fulfillments;
+
+use Automattic\WooCommerce\Internal\DataStores\Fulfillments\FulfillmentsDataStore;
+
+defined( 'ABSPATH' ) || exit;
+
+
+/**
+ * WC Order Fulfillment Class
+ *
+ * @since 9.9.0
+ */
+class Fulfillment extends \WC_Data {
+	/**
+	 * Fulfillment ID.
+	 *
+	 * @var int
+	 */
+	protected int $fulfillment_id = 0;
path: plugins/woocommerce/src/Internal/Fulfillments/Fulfillment.php
user: login: tpaksu
body: Addressed in https://github.com/woocommerce/woocommerce/pull/57429/commits/f07fcfde8335623fd46fd7adfdb1628421eaccf7.
created_at: 2025-04-25T00:53:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+<?php
+/**
+ * Class FulfillmentsDataStore file.
+ *
+ * @package WooCommerce\DataStores
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\DataStores\Fulfillments;
+
+use Automattic\WooCommerce\Internal\Fulfillments\Fulfillment;
+use WC_Meta_Data;
+use WP_Error;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * WC Order Item Product Data Store
+ *
+ * @version  9.9.0
+ */
+class FulfillmentsDataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface {
+
+	/**
+	 * Method to create a new fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to create.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data is invalid.
+	 * @throws \Exception If the fulfillment can't be created.
+	 */
+	public function create( &$data ): void {
+		// Validate the fulfillment data.
+		if ( ! $data->get_entity_type() ) {
+			throw new \Exception( esc_html__( 'Invalid entity type.', 'woocommerce' ) );
+		}
+		if ( ! $data->get_entity_id() ) {
+			throw new \Exception( esc_html__( 'Invalid entity ID.', 'woocommerce' ) );
+		}
+		if ( empty( $data->get_items() ) ) {
+			throw new \Exception( esc_html__( 'The fulfillment should contain at least one item.', 'woocommerce' ) );
+		}
+		foreach ( $data->get_items() as $item ) {
+			if ( ! isset( $item['item_id'] ) || ! isset( $item['qty'] ) ) {
+				throw new \Exception( esc_html__( 'Invalid item.', 'woocommerce' ) );
+			}
+		}
+
+		// Set fulfillment properties.
+		$data->set_date_updated( new \DateTime( current_time( 'mysql' ) ) );
+		$data->set_date_deleted( null );
+
+		// Save the fulfillment to the database.
+		global $wpdb;
+		$data_id = $wpdb->insert(
+			$wpdb->prefix . 'wc_order_fulfillments',
+			array(
+				'entity_type'  => $data->get_entity_type(),
+				'entity_id'    => $data->get_entity_id(),
+				'date_updated' => $data->get_date_updated()->format( 'Y-m-d H:i:s' ),
+				'date_deleted' => $data->get_date_deleted() ? $data->get_date_deleted()->format( 'Y-m-d H:i:s' ) : null,
+			),
+			array(
+				'%s',
+				'%d',
+				'%s',
+				'%s',
+			)
+		);
+
+		// Check for errors.
+		if ( false === $data_id ) {
+			throw new \Exception( esc_html__( 'Failed to insert fulfillment.', 'woocommerce' ) );
+		}
+
+		// Set the ID of the fulfillment object.
+		$data_id = $wpdb->insert_id;
+
+		$data->set_id( $data_id );
+
+		// Save the metadata for the fulfillment to the database.
+		$data->save_meta_data();
+
+		// Apply changes let's the object know that the current object reflects the database and no "changes" exist between the two.
+		$data->apply_changes();
+		$data->set_object_read( true );
+	}
+
+	/**
+	 * Method to read a fulfillment from the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to read.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data can't be read.
+	 */
+	public function read( &$data ): void {
+		// Read the fulfillment from the database.
+		global $wpdb;
+
+		$data_id          = $data->get_id();
+		$fulfillment_data = $wpdb->get_row(
path: plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStore.php
user: login: tpaksu
body: Addressed in [10d619f](https://github.com/woocommerce/woocommerce/pull/57429/commits/10d619f0ae1879dc45f153db06dd8a2d6b1b64f6)
created_at: 2025-04-25T00:54:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+<?php
+/**
+ * Class FulfillmentsDataStore file.
+ *
+ * @package WooCommerce\DataStores
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\DataStores\Fulfillments;
+
+use Automattic\WooCommerce\Internal\Fulfillments\Fulfillment;
+use WC_Meta_Data;
+use WP_Error;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * WC Order Item Product Data Store
+ *
+ * @version  9.9.0
+ */
+class FulfillmentsDataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface {
+
+	/**
+	 * Method to create a new fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to create.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data is invalid.
+	 * @throws \Exception If the fulfillment can't be created.
+	 */
+	public function create( &$data ): void {
+		// Validate the fulfillment data.
+		if ( ! $data->get_entity_type() ) {
+			throw new \Exception( esc_html__( 'Invalid entity type.', 'woocommerce' ) );
+		}
+		if ( ! $data->get_entity_id() ) {
+			throw new \Exception( esc_html__( 'Invalid entity ID.', 'woocommerce' ) );
+		}
+		if ( empty( $data->get_items() ) ) {
+			throw new \Exception( esc_html__( 'The fulfillment should contain at least one item.', 'woocommerce' ) );
+		}
+		foreach ( $data->get_items() as $item ) {
+			if ( ! isset( $item['item_id'] ) || ! isset( $item['qty'] ) ) {
+				throw new \Exception( esc_html__( 'Invalid item.', 'woocommerce' ) );
+			}
+		}
+
+		// Set fulfillment properties.
+		$data->set_date_updated( new \DateTime( current_time( 'mysql' ) ) );
+		$data->set_date_deleted( null );
+
+		// Save the fulfillment to the database.
+		global $wpdb;
+		$data_id = $wpdb->insert(
path: plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStore.php
user: login: tpaksu
body: Addressed in [10d619f](https://github.com/woocommerce/woocommerce/pull/57429/commits/10d619f0ae1879dc45f153db06dd8a2d6b1b64f6)
created_at: 2025-04-25T00:54:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+<?php
+/**
+ * Class FulfillmentsDataStore file.
+ *
+ * @package WooCommerce\DataStores
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\DataStores\Fulfillments;
+
+use Automattic\WooCommerce\Internal\Fulfillments\Fulfillment;
+use WC_Meta_Data;
+use WP_Error;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * WC Order Item Product Data Store
+ *
+ * @version  9.9.0
+ */
+class FulfillmentsDataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface {
+
+	/**
+	 * Method to create a new fulfillment in the database.
+	 *
+	 * @param Fulfillment $data The fulfillment object to create.
+	 *
+	 * @return void
+	 *
+	 * @throws \Exception If the fulfillment data is invalid.
+	 * @throws \Exception If the fulfillment can't be created.
+	 */
+	public function create( &$data ): void {
+		// Validate the fulfillment data.
+		if ( ! $data->get_entity_type() ) {
+			throw new \Exception( esc_html__( 'Invalid entity type.', 'woocommerce' ) );
+		}
+		if ( ! $data->get_entity_id() ) {
+			throw new \Exception( esc_html__( 'Invalid entity ID.', 'woocommerce' ) );
+		}
+		if ( empty( $data->get_items() ) ) {
+			throw new \Exception( esc_html__( 'The fulfillment should contain at least one item.', 'woocommerce' ) );
+		}
+		foreach ( $data->get_items() as $item ) {
+			if ( ! isset( $item['item_id'] ) || ! isset( $item['qty'] ) ) {
+				throw new \Exception( esc_html__( 'Invalid item.', 'woocommerce' ) );
+			}
+		}
+
+		// Set fulfillment properties.
+		$data->set_date_updated( new \DateTime( current_time( 'mysql' ) ) );
+		$data->set_date_deleted( null );
path: plugins/woocommerce/src/Internal/DataStores/Fulfillments/FulfillmentsDataStore.php
user: login: tpaksu
body: Addressed in [10d619f](https://github.com/woocommerce/woocommerce/pull/57429/commits/10d619f0ae1879dc45f153db06dd8a2d6b1b64f6).
created_at: 2025-04-25T00:54:31Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-21T22:31:32Z
body: ## Testing Guidelines
Hi @prettyboymp @jorgeatorres @allie500 ,

Apart from reviewing the code changes, please make sure to **review the testing instructions** ([Guide](https://github.com/woocommerce/woocommerce/wiki/Writing-high-quality-testing-instructions)) and verify that relevant tests (E2E, Unit, Integration, etc.) have been added or updated as needed.

**Reminder:** PR reviewers are required to document testing performed. This includes:
- 🖼️ **Screenshots or screen recordings**.
- 📝 **List of functionality tested / steps followed**.
- 🌐 **Site details** (environment attributes such as hosting type, plugins, theme, store size, store age, and relevant settings).
- 🔍 **Any analysis performed**, such as assessing potential impacts on environment attributes and other plugins, conducting performance profiling, or using LLM/AI-based analysis.

⚠️ Within the testing details you provide, please ensure that no sensitive information (such as API keys, passwords, user data, etc.) is included in this public issue.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-21T22:33:48Z
body: 
## Test using WordPress Playground
The changes in this pull request can be previewed and tested using a [WordPress Playground](https://developer.wordpress.org/playground/) instance.
[WordPress Playground](https://developer.wordpress.org/playground/) is an experimental project that creates a full WordPress instance entirely within the browser.

[Test this pull request with WordPress Playground](https://playground.wordpress.net/#{"landingPage":"/wp-admin/admin.php?page=wc-admin","preferredVersions":{"php":"8.0","wp":"latest"},"phpExtensionBundles":["kitchen-sink"],"features":{"networking":true},"steps":[{"step":"installPlugin","pluginZipFile":{"resource":"url","url":"https://playground.wordpress.net/plugin-proxy.php?org=woocommerce&repo=woocommerce&workflow=Build%20Live%20Branch&artifact=plugins-14667013832&pr=57429"},"options":{"activate":true}},{"step":"installPlugin","pluginZipFile":{"resource":"url","url":"https://github-proxy.com/https://github.com/woocommerce/woocommerce/releases/download/wc-beta-tester-2.3.1/woocommerce-beta-tester.zip"},"options":{"activate":true}},{"step":"login","username":"admin","password":"password"}],"plugins":[]}).

Note that this URL is valid for 30 days from when this comment was last updated. You can update it by closing/reopening the PR or pushing a new commit.

author_association: CONTRIBUTOR