* 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, $save = true ) { // XXX implement $save = true. return $order->update_meta_data( '_new_order_email_sent', wc_bool_to_string( $set ) ); } /** * Helper setter for email_sent. * * @param \WC_Order $order Order object. * * @return bool Whether email was sent. */ private function get_new_order_email_sent( $order ) { return $this->get_email_sent( $order ); } /** * Helper setter for new order email 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. * @return bool Whether email was sent. */ private function set_new_order_email_sent( $order, $set, $save = true ) { // XXX implement $save = true. return $this->set_email_sent( $order, $set ); } /** * Gets information about whether stock was reduced. * * @param \WC_Order $order Order object. * * @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 ) ); } /** * Stores information about whether stock was reduced. * * @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, $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 ) ); } //phpcs:disable Squiz.Commenting, Generic.Commenting // TODO: Add methods for other table names as appropriate. public function get_total_refunded( $order ) { // TODO: Implement get_total_refunded() method. return 0; } public function get_total_tax_refunded( $order ) { // TODO: Implement get_total_tax_refunded() method. return 0; } public function get_total_shipping_refunded( $order ) { // TODO: Implement get_total_shipping_refunded() method. return 0; } public function get_order_id_by_order_key( $order_key ) { // TODO: Implement get_order_id_by_order_key() method. return 0; } public function get_order_count( $status ) { // TODO: Implement get_order_count() method. return 0; } public function get_orders( $args = array() ) { // TODO: Implement get_orders() method. return array(); } public function get_unpaid_orders( $date ) { // TODO: Implement get_unpaid_orders() method. return array(); } public function search_orders( $term ) { // TODO: Implement search_orders() method. return array(); } public function get_order_type( $order_id ) { // TODO: Implement get_order_type() method. return 'shop_order'; } //phpcs:enable Squiz.Commenting, Generic.Commenting /** * Method to read an order from custom tables. * * @param \WC_Order $order Order object. * * @throws \Exception If passed order is invalid. */ 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' ) ); } $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'] ) ) { continue; } $prop_setter_function_name = "set_{$prop_details['name']}"; 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']}, false ); } } } $order->set_object_read( true ); } /** * Return order data for a single order ID. * * @param int $id Order ID. * * @return object|\WP_Error DB order object or WP_Error. */ private function get_order_data_for_id( $id ) { $results = $this->get_order_data_for_ids( array( $id ) ); return is_array( $results ) && count( $results ) > 0 ? $results[0] : $results; } /** * Return order data for multiple IDs. * * @param array $ids List of order IDs. * * @return array|object|null DB Order objects or error. */ private function get_order_data_for_ids( $ids ) { global $wpdb; $order_table_query = $this->get_order_table_select_statement(); $id_placeholder = implode( ', ', array_fill( 0, count( $ids ), '%d' ) ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_table_query is autogenerated and should already be prepared. return $wpdb->get_results( $wpdb->prepare( "$order_table_query WHERE wc_order.id in ( $id_placeholder )", $ids ) ); // phpcs:enable } /** * Helper method to generate combined select statement. * * @return string Select SQL statement to fetch order. */ private function get_order_table_select_statement() { $order_table = $this::get_orders_table_name(); $order_table_alias = 'wc_order'; $select_clause = $this->generate_select_clause_for_props( $order_table_alias, $this->order_column_mapping ); $billing_address_table_alias = 'address_billing'; $shipping_address_table_alias = 'address_shipping'; $op_data_table_alias = 'order_operational_data'; $billing_address_clauses = $this->join_billing_address_table_to_order_query( $order_table_alias, $billing_address_table_alias ); $shipping_address_clauses = $this->join_shipping_address_table_to_order_query( $order_table_alias, $shipping_address_table_alias ); $operational_data_clauses = $this->join_operational_data_table_to_order_query( $order_table_alias, $op_data_table_alias ); return " SELECT $select_clause, {$billing_address_clauses['select']}, {$shipping_address_clauses['select']}, {$operational_data_clauses['select']} FROM $order_table $order_table_alias LEFT JOIN {$billing_address_clauses['join']} LEFT JOIN {$shipping_address_clauses['join']} LEFT JOIN {$operational_data_clauses['join']} "; } /** * Helper method to generate join query for billing addresses in wc_address table. * * @param string $order_table_alias Alias for order table to use in join. * @param string $address_table_alias Alias for address table to use in join. * * @return array Select and join statements for billing address table. */ private function join_billing_address_table_to_order_query( $order_table_alias, $address_table_alias ) { return $this->join_address_table_order_query( 'billing', $order_table_alias, $address_table_alias ); } /** * Helper method to generate join query for shipping addresses in wc_address table. * * @param string $order_table_alias Alias for order table to use in join. * @param string $address_table_alias Alias for address table to use in join. * * @return array Select and join statements for shipping address table. */ private function join_shipping_address_table_to_order_query( $order_table_alias, $address_table_alias ) { return $this->join_address_table_order_query( 'shipping', $order_table_alias, $address_table_alias ); } /** * Helper method to generate join and select query for address table. * * @param string $address_type Type of address. Typically will be `billing` or `shipping`. * @param string $order_table_alias Alias of order table to use. * @param string $address_table_alias Alias for address table to use. * * @return array Select and join statements for address table. */ private function join_address_table_order_query( $address_type, $order_table_alias, $address_table_alias ) { global $wpdb; $address_table = $this::get_addresses_table_name(); $column_props_map = 'billing' === $address_type ? $this->billing_address_column_mapping : $this->shipping_address_column_mapping; $clauses = $this->generate_select_and_join_clauses( $order_table_alias, $address_table, $address_table_alias, $column_props_map ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $clauses['join'] and $address_table_alias are hardcoded. $clauses['join'] = $wpdb->prepare( "{$clauses['join']} AND $address_table_alias.address_type = %s", $address_type ); // phpcs:enable return array( 'select' => $clauses['select'], 'join' => $clauses['join'], ); } /** * Helper method to join order operational data table. * * @param string $order_table_alias Alias to use for order table. * @param string $operational_table_alias Alias to use for operational data table. * * @return array Select and join queries for operational data table. */ private function join_operational_data_table_to_order_query( $order_table_alias, $operational_table_alias ) { $operational_data_table = $this::get_operational_data_table_name(); return $this->generate_select_and_join_clauses( $order_table_alias, $operational_data_table, $operational_table_alias, $this->operational_data_column_mapping ); } /** * Helper method to generate join and select clauses. * * @param string $order_table_alias Alias for order table. * @param string $table Table to join. * @param string $table_alias Alias for table to join. * @param array[] $column_props_map Column to prop map for table to join. * * @return array Select and join queries. */ private function generate_select_and_join_clauses( $order_table_alias, $table, $table_alias, $column_props_map ) { // Add aliases to column names so they will be unique when fetching. $select_clause = $this->generate_select_clause_for_props( $table_alias, $column_props_map ); $join_clause = "$table $table_alias ON $table_alias.order_id = $order_table_alias.id"; return array( 'select' => $select_clause, 'join' => $join_clause, ); } /** * Helper method to generate select clause for props. * * @param string $table_alias Alias for table. * @param array[] $props Props to column mapping for table. * * @return string Select clause. */ private function generate_select_clause_for_props( $table_alias, $props ) { $select_clauses = array(); foreach ( $props as $column_name => $prop_details ) { $select_clauses[] = isset( $prop_details['name'] ) ? "$table_alias.$column_name as {$prop_details['name']}" : "$table_alias.$column_name as {$table_alias}_$column_name"; } 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 /** * @param \WC_Order $order */ public function create( &$order ) { throw new \Exception( 'Unimplemented' ); } /** * Method to update an order in the database. * * @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' ) && 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 ) { return array(); } public function get_coupon_held_keys_for_users( $order, $coupon_id = null ) { return array(); } public function set_coupon_held_keys( $order, $held_keys, $held_keys_for_user ) { throw new \Exception( 'Unimplemented' ); } public function release_held_coupons( $order, $save = true ) { throw new \Exception( 'Unimplemented' ); } public function query( $query_vars ) { return array(); } public function get_order_item_type( $order, $order_item_id ) { return 'line_item'; } //phpcs:enable Squiz.Commenting, Generic.Commenting /** * Get the SQL needed to create all the tables needed for the custom orders table feature. * * @return string */ public function get_database_schema() { global $wpdb; $collate = $wpdb->has_cap( 'collation' ) ? $wpdb->get_charset_collate() : ''; $orders_table_name = $this->get_orders_table_name(); $addresses_table_name = $this->get_addresses_table_name(); $operational_data_table_name = $this->get_operational_data_table_name(); $meta_table = $this->get_meta_table_name(); $sql = " CREATE TABLE $orders_table_name ( id bigint(20) unsigned auto_increment, status varchar(20) null, currency varchar(10) null, tax_amount decimal(26,8) null, total_amount decimal(26,8) null, customer_id bigint(20) unsigned null, billing_email varchar(320) null, date_created_gmt datetime null, date_updated_gmt datetime null, parent_order_id bigint(20) unsigned null, payment_method varchar(100) null, payment_method_title text null, transaction_id varchar(100) null, ip_address varchar(100) null, user_agent text null, PRIMARY KEY (id), KEY status (status), KEY date_created (date_created_gmt), KEY customer_id_billing_email (customer_id, billing_email) ) $collate; CREATE TABLE $addresses_table_name ( id bigint(20) unsigned auto_increment primary key, order_id bigint(20) unsigned NOT NULL, address_type varchar(20) null, first_name text null, last_name text null, company text null, address_1 text null, address_2 text null, city text null, state text null, postcode text null, country text null, email varchar(320) null, phone varchar(100) null, KEY order_id (order_id), KEY address_type_order_id (address_type, order_id) ) $collate; CREATE TABLE $operational_data_table_name ( id bigint(20) unsigned auto_increment primary key, order_id bigint(20) unsigned NULL, created_via varchar(100) NULL, woocommerce_version varchar(20) NULL, prices_include_tax tinyint(1) NULL, coupon_usages_are_counted tinyint(1) NULL, download_permission_granted tinyint(1) NULL, cart_hash varchar(100) NULL, new_order_email_sent tinyint(1) NULL, order_key varchar(100) NULL, order_stock_reduced tinyint(1) NULL, date_paid_gmt datetime NULL, date_completed_gmt datetime NULL, shipping_tax_amount decimal(26, 8) NULL, shipping_total_amount decimal(26, 8) NULL, discount_tax_amount decimal(26, 8) NULL, discount_total_amount decimal(26, 8) NULL, recorded_sales tinyint(1) NULL, KEY order_id (order_id), KEY order_key (order_key) ) $collate; CREATE TABLE $meta_table ( id bigint(20) unsigned auto_increment primary key, order_id bigint(20) unsigned null, meta_key varchar(255), meta_value text null, KEY meta_key_value (meta_key, meta_value(100)) ) $collate; "; 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', ); } }