[COT] Implement create() method in COT datastore (#33341)

This commit is contained in:
Jorge A. Torres 2022-06-29 08:54:29 -03:00 committed by GitHub
parent 04bef970ef
commit a80e0fe203
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 319 additions and 71 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Implement `create()` in COT datastore.

View File

@ -318,7 +318,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
),
'order_stock_reduced' => array(
'type' => 'bool',
'name' => 'stock_reduced',
'name' => 'order_stock_reduced',
),
'date_paid_gmt' => array(
'type' => 'date',
@ -539,7 +539,28 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
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 ) );
return $order->update_meta_data( '_order_stock_reduced', wc_bool_to_string( $set ) );
}
/**
* Helper getter for `order_stock_reduced`.
*
* @param \WC_Order $order Order object.
* @return bool Whether stock was reduced.
*/
private function get_order_stock_reduced( $order ) {
return $this->get_stock_reduced( $order );
}
/**
* Helper setter for `order_stock_reduced`.
*
* @param \WC_Order $order Order ID or order object.
* @param bool $set Whether stock was reduced.
* @param bool $save Whether to persist changes to db immediately or not.
*/
private function set_order_stock_reduced( $order, $set, $save = true ) {
return $this->set_stock_reduced( $order, $set, $save );
}
//phpcs:disable Squiz.Commenting, Generic.Commenting
@ -801,86 +822,144 @@ LEFT JOIN {$operational_data_clauses['join']}
* 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
* @throws \Exception If order data is not valid.
*
* @since 6.8.0
*/
protected function persist_order_to_db( $order, $only_changes = true ) {
protected function persist_order_to_db( &$order ) {
global $wpdb;
// XXX implement case $only_changes = false.
$changes = $only_changes ? $order->get_changes() : array();
$context = ( 0 === absint( $order->get_id() ) ) ? 'create' : 'update';
$data_sync = wc_get_container()->get( DataSynchronizer::class );
if ( 'create' === $context ) {
// XXX: do we want to add some backwards compat for 'woocommerce_new_order_data'?
$post_id = wp_insert_post(
array(
'post_type' => $data_sync->data_sync_is_enabled() ? 'shop_order' : $data_sync::PLACEHOLDER_ORDER_POST_TYPE,
'post_status' => 'draft',
)
);
if ( ! $post_id ) {
throw new \Exception( __( 'Could not create order in posts table.', 'woocommerce' ) );
}
$order->set_id( $post_id );
}
// Figure out what needs to be updated in the database.
$db_updates = array();
$db_updates = $this->get_db_rows_for_order( $order, $context, ( 'update' === $context ) );
// Persist changes.
foreach ( $db_updates as $update ) {
// Make sure 'data' and 'format' entries match before passing to $wpdb.
ksort( $update['data'] );
ksort( $update['format'] );
$result = empty( $update['where'] )
? $wpdb->insert( $update['table'], $update['data'], array_values( $update['format'] ) )
: $wpdb->update( $update['table'], $update['data'], $update['where'], array_values( $update['format'] ), $update['where_format'] );
if ( false === $result ) {
// translators: %s is a table name.
throw new \Exception( sprintf( __( 'Could not persist order to database table "%s".', 'woocommerce' ), $update['table'] ) );
}
}
// Backfill post record.
if ( $data_sync->data_sync_is_enabled() ) {
$this->backfill_post_record( $order );
}
}
/**
* Generates an array of rows with all the details required to insert or update an order in the database.
*
* @param \WC_Order $order The order.
* @param string $context The context: 'create' or 'update'.
* @param boolean $only_changes Whether to consider only changes in the order for generating the rows.
* @return array
* @throws \Exception When invalid data is found for the given context.
*
* @since 6.8.0
*/
protected function get_db_rows_for_order( $order, $context = 'create', $only_changes = false ): array {
$result = array();
// wc_orders.
$row = $this->get_db_row_from_order_changes( $changes, $this->order_column_mapping );
$row = $this->get_db_row_from_order( $order, $this->order_column_mapping, $only_changes );
if ( 'create' === $context && ! $row ) {
throw new \Exception( 'No data for new record.' ); // This shouldn't occur.
}
if ( $row ) {
$db_updates[] = array_merge(
array(
'table' => self::get_orders_table_name(),
'where' => array( 'id' => $order->get_id() ),
'where_format' => '%d',
),
$row
$result[] = array(
'table' => self::get_orders_table_name(),
'data' => array_merge( $row['data'], array( 'id' => $order->get_id() ) ),
'format' => array_merge( $row['format'], array( 'id' => '%d' ) ),
'where' => 'update' === $context ? array( 'id' => $order->get_id() ) : null,
'where_format' => 'update' === $context ? '%d' : null,
);
}
// 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
);
$row = $this->get_db_row_from_order( $order, $this->operational_data_column_mapping, $only_changes );
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
$result[] = array(
'table' => self::get_operational_data_table_name(),
'data' => array_merge( $row['data'], array( 'order_id' => $order->get_id() ) ),
'format' => array_merge( $row['format'], array( 'order_id' => '%d' ) ),
'where' => 'update' === $context ? array( 'order_id' => $order->get_id() ) : null,
'where_format' => 'update' === $context ? '%d' : null,
);
}
// 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'} );
$row = $this->get_db_row_from_order( $order, $this->{$address_type . '_address_column_mapping'}, $only_changes );
if ( $row ) {
$db_updates[] = array_merge(
array(
'table' => self::get_addresses_table_name(),
'where' => array(
$result[] = array(
'table' => self::get_addresses_table_name(),
'data' => array_merge(
$row['data'],
array(
'order_id' => $order->get_id(),
'address_type' => $address_type,
),
'where_format' => array( '%d', '%s' ),
)
),
$row
'format' => array_merge(
$row['format'],
array(
'order_id' => '%d',
'address_type' => '%s',
)
),
'where' => 'update' === $context
? array(
'order_id' => $order->get_id(),
'address_type' => $address_type,
)
: null,
'where_format' => 'update' === $context ? array( '%d', '%s' ) : null,
);
}
}
// Persist changes.
foreach ( $db_updates as $update ) {
$wpdb->update(
$update['table'],
$update['row'],
$update['where'],
array_values( $update['format'] ),
$update['where_format']
);
}
/**
* Allow third parties to include rows that need to be inserted/updated in custom tables when persisting an order.
*
* @since 6.8.0
*
* @param array Array of rows to be inserted/updated when persisting an order. Each entry should be an array with
* keys 'table', 'data' (the row), 'format' (row format), 'where' and 'where_format'.
* @param \WC_Order The order object.
* @param string The context of the operation: 'create' or 'update'.
*/
$ext_rows = apply_filters( 'woocommerce_orders_table_datastore_extra_db_rows_for_order', array(), $order, $context );
return array_merge( $result, $ext_rows );
}
/**
@ -888,11 +967,21 @@ LEFT JOIN {$operational_data_clauses['join']}
* `$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.
* @param \WC_Order $order Order.
* @param array $column_mapping Table column mapping.
* @param bool $only_changes Whether to consider only changes in the order object or all fields.
* @return array
*
* @since 6.8.0
*/
private function get_db_row_from_order_changes( $changes, $column_mapping ) {
protected function get_db_row_from_order( $order, $column_mapping, $only_changes = false ) {
$changes = $only_changes ? $order->get_changes() : array_merge( $order->get_data(), $order->get_changes() );
// XXX: manually persist some of the properties until the datastore/property design is finalized.
foreach ( $this->get_internal_data_store_keys() as $key ) {
$changes[ $key ] = $this->{"get_$key"}( $order );
}
$row = array();
$row_format = array();
@ -910,7 +999,7 @@ LEFT JOIN {$operational_data_clauses['join']}
}
return array(
'row' => $row,
'data' => $row,
'format' => $row_format,
);
}
@ -919,10 +1008,40 @@ LEFT JOIN {$operational_data_clauses['join']}
//phpcs:disable Squiz.Commenting, Generic.Commenting
/**
* Method to create an order in the database.
*
* @param \WC_Order $order
*/
public function create( &$order ) {
throw new \Exception( 'Unimplemented' );
if ( '' === $order->get_order_key() ) {
$order->set_order_key( wc_generate_order_key() );
}
$order->set_version( Constants::get_constant( 'WC_VERSION' ) );
$order->set_currency( $order->get_currency() ? $order->get_currency() : get_woocommerce_currency() );
if ( ! $order->get_date_created( 'edit' ) ) {
$order->set_date_created( time() );
}
$this->update_post_meta( $order );
$this->persist_order_to_db( $order );
$order->save_meta_data();
$order->apply_changes();
$this->clear_caches( $order );
/**
* Fires when a new order is created.
*
* @since 2.7.0
*
* @param int Order ID.
* @param \WC_Order Order object.
*/
do_action( 'woocommerce_new_order', $order->get_id(), $order );
}
/**
@ -931,8 +1050,6 @@ LEFT JOIN {$operational_data_clauses['join']}
* @param \WC_Order $order
*/
public function update( &$order ) {
global $wpdb;
// Before updating, ensure date paid is set if missing.
if (
! $order->get_date_paid( 'edit' )
@ -951,21 +1068,16 @@ LEFT JOIN {$operational_data_clauses['join']}
// 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' ) );
}
$this->update_post_meta( $order );
// Update with latest changes.
$changes = $order->get_changes();
$this->persist_order_to_db( $order, true );
$this->persist_order_to_db( $order );
// Update download permissions if necessary.
if ( array_key_exists( 'billing_email', $changes ) || array_key_exists( 'customer_id', $changes ) ) {
@ -985,6 +1097,29 @@ LEFT JOIN {$operational_data_clauses['join']}
do_action( 'woocommerce_update_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
}
/**
* Helper method that updates post meta based on an order object.
* Mostly used for backwards compatibility purposes in this datastore.
*
* @param \WC_Order $order Order object.
* @since 3.0.0
*/
protected function update_post_meta( &$order ) {
$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 ) ) );
}
}
// Sync some COT fields to meta keys for backwards compatibility.
foreach ( $this->get_internal_data_store_keys() as $key ) {
$this->{"set_$key"}( $order, $this->{"get_$key"}( $order ), false );
}
}
public function get_coupon_held_keys( $order, $coupon_id = null ) {
return array();
}
@ -1203,4 +1338,20 @@ CREATE TABLE $meta_table (
);
}
/**
* Returns keys currently handled by this datastore manually (not available through order properties).
*
* @return array List of keys.
*/
protected function get_internal_data_store_keys() {
// XXX: Finalize design -- will these be turned into props?
return array(
'order_stock_reduced',
'download_permissions_granted',
'new_order_email_sent',
'recorded_sales',
'recorded_coupon_usage_counts',
);
}
}

View File

@ -38,7 +38,6 @@ class OrdersDataStoreServiceProvider extends AbstractServiceProvider {
*/
public function register() {
$this->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 ) );

View File

@ -195,6 +195,100 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
}
}
/**
* Tests create() on the COT datastore.
*/
public function test_cot_datastore_create() {
$order = new WC_Order();
$this->switch_data_store( $order, $this->sut );
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product();
$order->set_status( 'pending' );
$order->set_created_via( 'unit-tests' );
$order->set_currency( 'COP' );
$order->set_customer_ip_address( '127.0.0.1' );
$item = new WC_Order_Item_Product();
$item->set_props(
array(
'product' => $product,
'quantity' => 2,
'subtotal' => wc_get_price_excluding_tax( $product, array( 'qty' => 2 ) ),
'total' => wc_get_price_excluding_tax( $product, array( 'qty' => 2 ) ),
)
);
$order->add_item( $item );
$order->set_billing_first_name( 'Jeroen' );
$order->set_billing_last_name( 'Sormani' );
$order->set_billing_company( 'WooCompany' );
$order->set_billing_address_1( 'WooAddress' );
$order->set_billing_address_2( '' );
$order->set_billing_city( 'WooCity' );
$order->set_billing_state( 'NY' );
$order->set_billing_postcode( '123456' );
$order->set_billing_country( 'US' );
$order->set_billing_email( 'admin@example.org' );
$order->set_billing_phone( '555-32123' );
$payment_gateways = WC()->payment_gateways->payment_gateways();
$order->set_payment_method( $payment_gateways['bacs'] );
$order->set_shipping_total( 5.0 );
$order->set_discount_total( 0.0 );
$order->set_discount_tax( 0.0 );
$order->set_cart_tax( 0.0 );
$order->set_shipping_tax( 0.0 );
$order->set_total( 25.0 );
$order->save();
$order->get_data_store()->set_stock_reduced( $order, true, false );
$order->update_meta_data( 'my_meta', rand( 0, 255 ) );
$order_id = $order->save();
$this->assertIsInteger( $order_id );
$this->assertLessThan( $order_id, 0 );
wp_cache_flush();
// Read the order again (fresh).
$r_order = new WC_Order();
$r_order->set_id( $order_id );
$this->switch_data_store( $r_order, $this->sut );
$this->sut->read( $r_order );
// Compare some of the prop/meta values to those that should've been persisted.
$props_to_compare = array(
'status',
'created_via',
'currency',
'customer_ip_address',
'billing_first_name',
'billing_last_name',
'billing_company',
'billing_address_1',
'billing_city',
'billing_state',
'billing_postcode',
'billing_country',
'billing_email',
'billing_phone',
'shipping_total',
'total',
);
foreach ( $props_to_compare as $prop ) {
$this->assertEquals( $order->{"get_$prop"}( 'edit' ), $r_order->{"get_$prop"}( 'edit' ) );
}
$this->assertEquals( $order->get_meta( 'my_meta', true, 'edit' ), $r_order->get_meta( 'my_meta', true, 'edit' ) );
$this->assertEquals( $this->sut->get_stock_reduced( $order ), $this->sut->get_stock_reduced( $r_order ) );
}
/**
* Helper function to delete all meta for post.
*