From 4b63f348415b856566d8f56763c1874dcc8cff96 Mon Sep 17 00:00:00 2001 From: "Jorge A. Torres" Date: Wed, 15 Jun 2022 05:39:41 -0300 Subject: [PATCH] [COT] Implement metadata CRUD and `update()` method in COT datastore (#33026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add `OrdersTableDataStoreMeta` to handle metadata for orders * Add `OrdersTableDataStoreHelper` with various helper functions used in the COT datastore * Pass some helper classes as args to the COT datastore * Use `OrdersTableDataStoreMeta` for meta in COT datastore * Minor fixes to columns definition in COT datastore * First pass at update() in the COT datastore * PHPCS fixes * Remove duplicate `read_meta` calls. * Register `OrdersTableDataStore` earlier to make container happy * Do not hardcode table metadata in `OrdersTableDataStoreMeta` * Correctly format decimals for storing in the db * read() shouldn’t success on non-existing orders * Rework persisting to db in OrdersTableDataStore * Correctly handle some props in OrdersTableDataStore * Add changelog * Add missing TODOs * Remove unused variables * No need to query db before deleting meta in `OrdersTableDataStoreMeta` * Simplify OrdersTableDataStoreMeta::update_meta() * Explicitly enumerate columns in OrdersTableDataStoreMeta::get_metadata_by_id() * Make COT metadata implementation more generic * Do not use property_exists() to determine existence of meta value * Move some methods over to DatabaseUtil and get rid of COT datastore helper * Rename `CustomDataStoreMeta` to `CustomMetaDataStore` * Make PHPCS happy * Add unit test. * Correct arg passed to persist_order_to_db() * Remove comment * Split conditional on multiple lines Co-authored-by: vedanshujain --- .../changelog/add-32667-cot-data-store-update | 4 + .../DataStores/CustomMetaDataStore.php | 198 +++++++++ .../Orders/OrdersTableDataStore.php | 390 ++++++++++++++++-- .../Orders/OrdersTableDataStoreMeta.php | 33 ++ .../OrdersDataStoreServiceProvider.php | 6 + .../src/Internal/Utilities/DatabaseUtil.php | 60 +++ .../Orders/OrdersTableDataStoreTests.php | 62 +++ 7 files changed, 709 insertions(+), 44 deletions(-) create mode 100644 plugins/woocommerce/changelog/add-32667-cot-data-store-update create mode 100644 plugins/woocommerce/src/Internal/DataStores/CustomMetaDataStore.php create mode 100644 plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStoreMeta.php diff --git a/plugins/woocommerce/changelog/add-32667-cot-data-store-update b/plugins/woocommerce/changelog/add-32667-cot-data-store-update new file mode 100644 index 00000000000..9c25773587e --- /dev/null +++ b/plugins/woocommerce/changelog/add-32667-cot-data-store-update @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Implements update/saving logic for orders in the COT datastore. diff --git a/plugins/woocommerce/src/Internal/DataStores/CustomMetaDataStore.php b/plugins/woocommerce/src/Internal/DataStores/CustomMetaDataStore.php new file mode 100644 index 00000000000..0762bd89b67 --- /dev/null +++ b/plugins/woocommerce/src/Internal/DataStores/CustomMetaDataStore.php @@ -0,0 +1,198 @@ + $this->get_table_name(), + 'meta_id_field' => $this->get_meta_id_field(), + 'object_id_field' => $this->get_object_id_field(), + ); + } + + /** + * Returns an array of meta for an object. + * + * @param WC_Data $object WC_Data object. + * @return array + */ + public function read_meta( &$object ) { + global $wpdb; + + $db_info = $this->get_db_info(); + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $raw_meta_data = $wpdb->get_results( + $wpdb->prepare( + "SELECT {$db_info['meta_id_field']} AS meta_id, meta_key, meta_value FROM {$db_info['table']} WHERE {$db_info['object_id_field']} = %d ORDER BY meta_id", + $object->get_id() + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + + return $raw_meta_data; + } + + /** + * Deletes meta based on meta ID. + * + * @param WC_Data $object WC_Data object. + * @param stdClass $meta (containing at least ->id). + */ + public function delete_meta( &$object, $meta ) { + global $wpdb; + + if ( ! isset( $meta->id ) ) { + return false; + } + + $db_info = $this->get_db_info(); + $meta_id = absint( $meta->id ); + + return (bool) $wpdb->delete( $db_info['table'], array( $db_info['meta_id_field'] => $meta_id ) ); + } + + /** + * Add new piece of meta. + * + * @param WC_Data $object WC_Data object. + * @param stdClass $meta (containing ->key and ->value). + * @return int meta ID + */ + public function add_meta( &$object, $meta ) { + global $wpdb; + + if ( ! is_a( $meta, 'WC_Meta_Data' ) ) { + return false; + } + + $db_info = $this->get_db_info(); + + $object_id = $object->get_id(); + $meta_key = wp_unslash( wp_slash( $meta->key ) ); + $meta_value = maybe_serialize( is_string( $meta->value ) ? wp_unslash( wp_slash( $meta->value ) ) : $meta->value ); + + // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_value,WordPress.DB.SlowDBQuery.slow_db_query_meta_key + $result = $wpdb->insert( + $db_info['table'], + array( + $db_info['object_id_field'] => $object_id, + 'meta_key' => $meta_key, + 'meta_value' => $meta_value, + ) + ); + // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_value,WordPress.DB.SlowDBQuery.slow_db_query_meta_key + + return $result ? (int) $wpdb->insert_id : false; + } + + /** + * Update meta. + * + * @param WC_Data $object WC_Data object. + * @param stdClass $meta (containing ->id, ->key and ->value). + */ + public function update_meta( &$object, $meta ) { + global $wpdb; + + if ( ! isset( $meta->id ) || empty( $meta->key ) ) { + return false; + } + + // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_value,WordPress.DB.SlowDBQuery.slow_db_query_meta_key + $data = array( + 'meta_key' => $meta->key, + 'meta_value' => maybe_serialize( $meta->value ), + ); + // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_value,WordPress.DB.SlowDBQuery.slow_db_query_meta_key + + $db_info = $this->get_db_info(); + + $result = $wpdb->update( + $db_info['table'], + $data, + array( $db_info['meta_id_field'] => $meta->id ), + '%s', + '%d' + ); + + return 1 === $result; + } + + /** + * Retrieves metadata by meta ID. + * + * @param int $meta_id Meta ID. + * @return object|bool Metadata object or FALSE if not found. + */ + public function get_metadata_by_id( $meta_id ) { + global $wpdb; + + if ( ! is_numeric( $meta_id ) || floor( $meta_id ) != $meta_id ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison + return false; + } + + $db_info = $this->get_db_info(); + + $meta_id = absint( $meta_id ); + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $meta = $wpdb->get_row( + $wpdb->prepare( + "SELECT {$db_info['meta_id_field']}, meta_key, meta_value, {$db_info['object_id_field']} FROM {$db_info['table']} WHERE {$db_info['meta_id_field']} = %d", + $meta_id + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + + if ( empty( $meta ) ) { + return false; + } + + if ( isset( $meta->meta_value ) ) { + $meta->meta_value = maybe_unserialize( $meta->meta_value ); + } + + return $meta; + } + +} diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index 0bfbf63ade4..86aa9f0d357 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -5,6 +5,9 @@ namespace Automattic\WooCommerce\Internal\DataStores\Orders; +use Automattic\Jetpack\Constants; +use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil; + defined( 'ABSPATH' ) || exit; /** @@ -12,6 +15,34 @@ defined( 'ABSPATH' ) || exit; */ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements \WC_Object_Data_Store_Interface, \WC_Order_Data_Store_Interface { + /** + * Handles custom metadata in the wc_orders_meta table. + * + * @var OrdersTableDataStoreMeta + */ + protected $data_store_meta; + + /** + * The database util object to use. + * + * @var DatabaseUtil + */ + protected $database_util; + + + /** + * Initialize the object. + * + * @internal + * @param OrdersTableDataStoreMeta $data_store_meta Metadata instance. + * @param DatabaseUtil $database_util The database util instance to use. + * @return void + */ + final public function init( OrdersTableDataStoreMeta $data_store_meta, DatabaseUtil $database_util ) { + $this->data_store_meta = $data_store_meta; + $this->database_util = $database_util; + } + /** * Get the custom orders table name. * @@ -101,7 +132,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements 'name' => 'customer_id', ), 'billing_email' => array( - 'type' => 'int', + 'type' => 'string', 'name' => 'billing_email', ), 'date_created_gmt' => array( @@ -262,7 +293,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements 'name' => 'version', ), 'prices_include_tax' => array( - 'type' => 'string', + 'type' => 'bool', 'name' => 'prices_include_tax', ), 'coupon_usages_are_counted' => array( @@ -278,7 +309,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements 'name' => 'cart_hash', ), 'new_order_email_sent' => array( - 'type' => 'string', + 'type' => 'bool', 'name' => 'new_order_email_sent', ), 'order_key' => array( @@ -313,7 +344,10 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements 'type' => 'decimal', 'name' => 'discount_total', ), - 'recorded_sales' => array( 'type' => 'bool' ), + 'recorded_sales' => array( + 'type' => 'bool', + 'name' => 'recorded_sales', + ), ); /** @@ -384,8 +418,10 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements * * @param \WC_Order $order Order ID or order object. * @param bool $set True or false. + * @param bool $save Whether to persist changes to db immediately or not. */ - public function set_download_permissions_granted( $order, $set ) { + public function set_download_permissions_granted( $order, $set, $save = true ) { + // XXX implement $save = true. return $order->update_meta_data( '_download_permissions_granted', wc_bool_to_string( $set ) ); } @@ -405,8 +441,10 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements * * @param \WC_Order $order Order object. * @param bool $set True or false. + * @param bool $save Whether to persist changes to db immediately or not. */ - public function set_recorded_sales( $order, $set ) { + public function set_recorded_sales( $order, $set, $save = true ) { + // XXX implement $save = true. return $order->update_meta_data( '_recorded_sales', wc_bool_to_string( $set ) ); } @@ -426,8 +464,9 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements * * @param \WC_Order $order Order object. * @param bool $set True or false. + * @param bool $save Whether to persist changes to db immediately or not. */ - public function set_recorded_coupon_usage_counts( $order, $set ) { + public function set_recorded_coupon_usage_counts( $order, $set, $save = true ) { return $order->update_meta_data( '_recorded_coupon_usage_counts', wc_bool_to_string( $set ) ); } @@ -446,10 +485,11 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements * Stores information about whether email was sent. * * @param \WC_Order $order Order object. - * * @param bool $set True or false. + * @param bool $save Whether to persist changes to db immediately or not. */ - public function set_email_sent( $order, $set ) { + public function set_email_sent( $order, $set, $save = true ) { + // XXX implement $save = true. return $order->update_meta_data( '_new_order_email_sent', wc_bool_to_string( $set ) ); } @@ -469,10 +509,11 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements * * @param \WC_Order $order Order object. * @param bool $set True or false. - * + * @param bool $save Whether to persist changes to db immediately or not. * @return bool Whether email was sent. */ - private function set_new_order_email_sent( $order, $set ) { + private function set_new_order_email_sent( $order, $set, $save = true ) { + // XXX implement $save = true. return $this->set_email_sent( $order, $set ); } @@ -484,6 +525,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements * @return bool Whether stock was reduced. */ public function get_stock_reduced( $order ) { + $order = is_numeric( $order ) ? wc_get_order( $order ) : $order; return wc_string_to_bool( $order->get_meta( '_order_stock_reduced', true ) ); } @@ -492,8 +534,11 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements * * @param \WC_Order $order Order ID or order object. * @param bool $set True or false. + * @param bool $save Whether to persist changes to db immediately or not. */ - public function set_stock_reduced( $order, $set ) { + public function set_stock_reduced( $order, $set, $save = true ) { + // XXX implement $save = true. + $order = is_numeric( $order ) ? wc_get_order( $order ) : $order; return $order->update_meta_data( '_order_stock_reduced', wc_string_to_bool( $set ) ); } @@ -557,10 +602,16 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements public function read( &$order ) { $order->set_defaults(); if ( ! $order->get_id() ) { - throw new \Exception( __( 'ID must be set for an order to be read', 'woocommerce' ) ); + throw new \Exception( __( 'ID must be set for an order to be read.', 'woocommerce' ) ); } - $order->read_meta_data(); + $order_data = $this->get_order_data_for_id( $order->get_id() ); + if ( ! $order_data ) { + throw new \Exception( __( 'Invalid order.', 'woocommerce' ) ); + } + + $order->read_meta_data(); + foreach ( $this->get_all_order_column_mappings() as $table_name => $column_mapping ) { foreach ( $column_mapping as $column_name => $prop_details ) { if ( ! isset( $prop_details['name'] ) ) { @@ -570,39 +621,12 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements if ( is_callable( array( $order, $prop_setter_function_name ) ) ) { $order->{$prop_setter_function_name}( $order_data->{$prop_details['name']} ); } elseif ( is_callable( array( $this, $prop_setter_function_name ) ) ) { - $this->{$prop_setter_function_name}( $order, $order_data->{$prop_details['name']} ); + $this->{$prop_setter_function_name}( $order, $order_data->{$prop_details['name']}, false ); } } } - $order->set_object_read(); - } - - /** - * Read metadata directly from database. - * - * @param \WC_Order $order Order object. - * - * @return array Metadata array. - */ - public function read_meta( &$order ) { - global $wpdb; - $meta_table = $this::get_meta_table_name(); - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $meta_table is hardcoded. - $raw_meta_data = $wpdb->get_results( - $wpdb->prepare( - " -SELECT id as meta_id, meta_key, meta_value -FROM $meta_table -WHERE order_id = %d -ORDER BY meta_id; -", - $order->get_id() - ) - ); - // phpcs:enable - - return $this->filter_raw_meta_data( $order, $raw_meta_data ); + $order->set_object_read( true ); } /** @@ -773,6 +797,124 @@ LEFT JOIN {$operational_data_clauses['join']} return implode( ', ', $select_clauses ); } + /** + * Persists order changes to the database. + * + * @param \WC_Order $order The order. + * @param boolean $only_changes Whether to persist all order data or just changes in the object. + * @return void + */ + protected function persist_order_to_db( $order, $only_changes = true ) { + global $wpdb; + + // XXX implement case $only_changes = false. + $changes = $only_changes ? $order->get_changes() : array(); + + // Figure out what needs to be updated in the database. + $db_updates = array(); + + // wc_orders. + $row = $this->get_db_row_from_order_changes( $changes, $this->order_column_mapping ); + if ( $row ) { + $db_updates[] = array_merge( + array( + 'table' => self::get_orders_table_name(), + 'where' => array( 'id' => $order->get_id() ), + 'where_format' => '%d', + ), + $row + ); + } + + // wc_order_operational_data. + $row = $this->get_db_row_from_order_changes( + array_merge( + $changes, + // XXX: manually persist some of the properties until the datastore/property design is finalized. + array( + 'stock_reduced' => $this->get_stock_reduced( $order ), + 'download_permissions_granted' => $this->get_download_permissions_granted( $order ), + 'new_order_email_sent' => $this->get_email_sent( $order ), + 'recorded_sales' => $this->get_recorded_sales( $order ), + 'recorded_coupon_usage_counts' => $this->get_recorded_coupon_usage_counts( $order ), + ) + ), + $this->operational_data_column_mapping + ); + if ( $row ) { + $db_updates[] = array_merge( + array( + 'table' => self::get_operational_data_table_name(), + 'where' => array( 'order_id' => $order->get_id() ), + 'where_format' => '%d', + ), + $row + ); + } + + // wc_order_addresses. + foreach ( array( 'billing', 'shipping' ) as $address_type ) { + $row = $this->get_db_row_from_order_changes( $changes, $this->{$address_type . '_address_column_mapping'} ); + + if ( $row ) { + $db_updates[] = array_merge( + array( + 'table' => self::get_addresses_table_name(), + 'where' => array( + 'order_id' => $order->get_id(), + 'address_type' => $address_type, + ), + 'where_format' => array( '%d', '%s' ), + ), + $row + ); + } + } + + // Persist changes. + foreach ( $db_updates as $update ) { + $wpdb->update( + $update['table'], + $update['row'], + $update['where'], + array_values( $update['format'] ), + $update['where_format'] + ); + } + } + + /** + * Produces an array with keys 'row' and 'format' that can be passed to `$wpdb->update()` as the `$data` and + * `$format` parameters. Values are taken from the order changes array and properly formatted for inclusion in the + * database. + * + * @param array $changes Order changes array. + * @param array $column_mapping Table column mapping. + * @return array + */ + private function get_db_row_from_order_changes( $changes, $column_mapping ) { + $row = array(); + $row_format = array(); + + foreach ( $column_mapping as $column => $details ) { + if ( ! isset( $details['name'] ) || ! array_key_exists( $details['name'], $changes ) ) { + continue; + } + + $row[ $column ] = $this->database_util->format_object_value_for_db( $changes[ $details['name'] ], $details['type'] ); + $row_format[ $column ] = $this->database_util->get_wpdb_format_for_type( $details['type'] ); + } + + if ( ! $row ) { + return false; + } + + return array( + 'row' => $row, + 'format' => $row_format, + ); + } + //phpcs:disable Squiz.Commenting, Generic.Commenting @@ -783,8 +925,64 @@ LEFT JOIN {$operational_data_clauses['join']} throw new \Exception( 'Unimplemented' ); } + /** + * Method to update an order in the database. + * + * @param \WC_Order $order + */ public function update( &$order ) { - throw new \Exception( 'Unimplemented' ); + global $wpdb; + + // Before updating, ensure date paid is set if missing. + if ( + ! $order->get_date_paid( 'edit' ) + && version_compare( $order->get_version( 'edit' ), '3.0', '<' ) + && $order->has_status( apply_filters( 'woocommerce_payment_complete_order_status', $order->needs_processing() ? 'processing' : 'completed', $order->get_id(), $order ) ) // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment + ) { + $order->set_date_paid( $order->get_date_created( 'edit' ) ); + } + + if ( null === $order->get_date_created( 'edit' ) ) { + $order->set_date_created( time() ); + } + + $order->set_version( Constants::get_constant( 'WC_VERSION' ) ); + + // Fetch changes. + $changes = $order->get_changes(); + + // If address changed, store concatenated version to make searches faster. + foreach ( array( 'billing', 'shipping' ) as $address_type ) { + if ( isset( $changes[ $address_type ] ) ) { + $order->update_meta_data( "_{$address_type}_address_index", implode( ' ', $order->get_address( $address_type ) ) ); + } + } + + if ( ! isset( $changes['date_modified'] ) ) { + $order->set_date_modified( gmdate( 'Y-m-d H:i:s' ) ); + } + + // Update with latest changes. + $changes = $order->get_changes(); + + $this->persist_order_to_db( $order, true ); + + // Update download permissions if necessary. + if ( array_key_exists( 'billing_email', $changes ) || array_key_exists( 'customer_id', $changes ) ) { + $data_store = \WC_Data_Store::load( 'customer-download' ); + $data_store->update_user_by_order_id( $order->get_id(), $order->get_customer_id(), $order->get_billing_email() ); + } + + // Mark user account as active. + if ( array_key_exists( 'customer_id', $changes ) ) { + wc_update_user_last_active( $order->get_customer_id() ); + } + + $order->save_meta_data(); + $order->apply_changes(); + $this->clear_caches( $order ); + + do_action( 'woocommerce_update_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment } public function get_coupon_held_keys( $order, $coupon_id = null ) { @@ -901,4 +1099,108 @@ CREATE TABLE $meta_table ( return $sql; } + + /** + * Returns an array of meta for an object. + * + * @param WC_Data $object WC_Data object. + * @return array + */ + public function read_meta( &$object ) { + return $this->data_store_meta->read_meta( $object ); + } + + /** + * Deletes meta based on meta ID. + * + * @param WC_Data $object WC_Data object. + * @param stdClass $meta (containing at least ->id). + */ + public function delete_meta( &$object, $meta ) { + return $this->data_store_meta->delete_meta( $object, $meta ); + } + + /** + * Add new piece of meta. + * + * @param WC_Data $object WC_Data object. + * @param stdClass $meta (containing ->key and ->value). + * @return int meta ID + */ + public function add_meta( &$object, $meta ) { + return $this->data_store_meta->add_meta( $object, $meta ); + } + + /** + * Update meta. + * + * @param WC_Data $object WC_Data object. + * @param stdClass $meta (containing ->id, ->key and ->value). + */ + public function update_meta( &$object, $meta ) { + return $this->data_store_meta->update_meta( $object, $meta ); + } + + /** + * Returns list of metadata that is considered "internal". + * + * @return array + */ + public function get_internal_meta_keys() { + // XXX: This is mostly just to trick `WC_Data_Store_WP` for the time being. + return array( + '_customer_user', + '_order_key', + '_order_currency', + '_billing_first_name', + '_billing_last_name', + '_billing_company', + '_billing_address_1', + '_billing_address_2', + '_billing_city', + '_billing_state', + '_billing_postcode', + '_billing_country', + '_billing_email', + '_billing_phone', + '_shipping_first_name', + '_shipping_last_name', + '_shipping_company', + '_shipping_address_1', + '_shipping_address_2', + '_shipping_city', + '_shipping_state', + '_shipping_postcode', + '_shipping_country', + '_shipping_phone', + '_completed_date', + '_paid_date', + '_edit_lock', + '_edit_last', + '_cart_discount', + '_cart_discount_tax', + '_order_shipping', + '_order_shipping_tax', + '_order_tax', + '_order_total', + '_payment_method', + '_payment_method_title', + '_transaction_id', + '_customer_ip_address', + '_customer_user_agent', + '_created_via', + '_order_version', + '_prices_include_tax', + '_date_completed', + '_date_paid', + '_payment_tokens', + '_billing_address_index', + '_shipping_address_index', + '_recorded_sales', + '_recorded_coupon_usage_counts', + '_download_permissions_granted', + '_order_stock_reduced', + ); + } + } diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStoreMeta.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStoreMeta.php new file mode 100644 index 00000000000..9be17663bd7 --- /dev/null +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStoreMeta.php @@ -0,0 +1,33 @@ +share( OrdersTableDataStoreMeta::class ); + $this->share( OrdersTableDataStoreHelper::class ); + + $this->share( OrdersTableDataStore::class )->addArguments( array( OrdersTableDataStoreMeta::class, DatabaseUtil::class ) ); $this->share( DataSynchronizer::class )->addArguments( array( OrdersTableDataStore::class, DatabaseUtil::class, PostsToOrdersMigrationController::class ) ); $this->share( CustomOrdersTableController::class )->addArguments( array( OrdersTableDataStore::class, DataSynchronizer::class ) ); $this->share( OrdersTableDataStore::class ); diff --git a/plugins/woocommerce/src/Internal/Utilities/DatabaseUtil.php b/plugins/woocommerce/src/Internal/Utilities/DatabaseUtil.php index 526edd41007..1f2966969fb 100644 --- a/plugins/woocommerce/src/Internal/Utilities/DatabaseUtil.php +++ b/plugins/woocommerce/src/Internal/Utilities/DatabaseUtil.php @@ -133,4 +133,64 @@ AND index_name='$index_name'" ); // phpcs:enable WordPress.DB.PreparedSQL } + + /** + * Formats an object value of type `$type` for inclusion in the database. + * + * @param mixed $value Raw value. + * @param string $type Data type. + * @return mixed + * @throws \Exception When an invalid type is passed. + */ + public function format_object_value_for_db( $value, string $type ) { + switch ( $type ) { + case 'decimal': + $value = wc_format_decimal( $value, false, true ); + break; + case 'int': + $value = (int) $value; + break; + case 'bool': + $value = wc_string_to_bool( $value ); + break; + case 'string': + $value = strval( $value ); + break; + case 'date': + $value = $value ? ( new \DateTime( $value ) )->format( 'Y-m-d H:i:s' ) : null; + break; + case 'date_epoch': + $value = $value ? ( new \DateTime( "@{$value}" ) )->format( 'Y-m-d H:i:s' ) : null; + break; + default: + throw new \Exception( 'Invalid type received: ' . $type ); + } + + return $value; + } + + /** + * Returns the `$wpdb` placeholder to use for data type `$type`. + * + * @param string $type Data type. + * @return string + * @throws \Exception When an invalid type is passed. + */ + public function get_wpdb_format_for_type( string $type ) { + static $wpdb_placeholder_for_type = array( + 'int' => '%d', + 'decimal' => '%f', + 'string' => '%s', + 'date' => '%s', + 'date_epoch' => '%s', + 'bool' => '%d', + ); + + if ( ! isset( $wpdb_placeholder_for_type[ $type ] ) ) { + throw new \Exception( 'Invalid column type: ' . $type ); + } + + return $wpdb_placeholder_for_type[ $type ]; + } + } diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php index 5a41d4e70cc..103a5f1291f 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php @@ -133,6 +133,68 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { } } + /** + * Tests update() on the COT datastore. + */ + public function test_cot_datastore_update() { + static $props_to_update = array( + 'billing_first_name' => 'John', + 'billing_last_name' => 'Doe', + 'shipping_phone' => '555-55-55', + 'status' => 'on-hold', + 'cart_hash' => 'YET-ANOTHER-CART-HASH', + ); + static $datastore_updates = array( + 'email_sent' => true, + ); + static $meta_to_update = array( + 'my_meta_key' => array( 'my', 'custom', 'meta' ), + ); + + // Set up order. + $post_order = OrderHelper::create_order(); + $this->migrator->migrate_orders( array( $post_order->get_id() ) ); + + // Read order using the COT datastore. + wp_cache_flush(); + $order = new WC_Order(); + $order->set_id( $post_order->get_id() ); + $this->switch_data_store( $order, $this->sut ); + $this->sut->read( $order ); + + // Make some changes to the order and save. + $order->set_props( $props_to_update ); + + foreach ( $meta_to_update as $meta_key => $meta_value ) { + $order->add_meta_data( $meta_key, $meta_value, true ); + } + + foreach ( $datastore_updates as $prop => $value ) { + $this->sut->{"set_$prop"}( $order, $value ); + } + + $order->save(); + + // Re-read order and make sure changes were persisted. + wp_cache_flush(); + $order = new WC_Order(); + $order->set_id( $post_order->get_id() ); + $this->switch_data_store( $order, $this->sut ); + $this->sut->read( $order ); + + foreach ( $props_to_update as $prop => $value ) { + $this->assertEquals( $order->{"get_$prop"}( 'edit' ), $value ); + } + + foreach ( $meta_to_update as $meta_key => $meta_value ) { + $this->assertEquals( $order->get_meta( $meta_key, true, 'edit' ), $meta_value ); + } + + foreach ( $datastore_updates as $prop => $value ) { + $this->assertEquals( $this->sut->{"get_$prop"}( $order ), $value ); + } + } + /** * Helper function to delete all meta for post. *