0, 'refunds' => 0, ); $products = $ctx->pool( 'products' ); $customers = $ctx->pool( 'customers' ); $coupons = $ctx->pool( 'coupons' ); if ( ! $products ) { return $none; } $random = Random::for( $ctx->batch, 'orders', $index ); $plan = Plan::order( $random, array( 'statuses' => $ctx->statuses, 'guest_share' => $ctx->guest_share, 'customers' => count( $customers ), 'products' => count( $products ), 'coupons' => count( $coupons ), ) ); /** This filter is documented in src/Modules/TestData/Make/Products.php */ $plan = (array) apply_filters( 'apwc_test_data_item', $plan, 'order', $index, $ctx->batch ); $status = isset( Plan::STATUSES[ $plan['status'] ?? '' ] ) ? (string) $plan['status'] : 'completed'; // Checked again after the filter: a refunded order is fully refunded, a partial refund only // on a completed or processing order (a full refund on another status would change it). if ( 'refunded' === $status ) { $plan['refund'] = 'full'; } elseif ( ! in_array( $status, array( 'completed', 'processing' ), true ) || 'partial' !== ( $plan['refund'] ?? '' ) ) { $plan['refund'] = ''; } $customer = null; $from = $ctx->from; if ( empty( $plan['guest'] ) && $customers ) { $user_id = $customers[ max( 0, min( count( $customers ) - 1, (int) ( $plan['customer'] ?? 0 ) ) ) ]; $customer = new \WC_Customer( $user_id ); if ( ! $customer->get_id() ) { $customer = null; } else { $registered = $customer->get_date_created(); $from = max( $from, $registered ? $registered->getTimestamp() : $from ); } } $date = Plan::date_in( (float) ( $plan['date_roll'] ?? 0.5 ), $from, $ctx->to ); $order = new \WC_Order(); $order->update_meta_data( Marker::META, $ctx->batch ); $order->set_created_via( Marker::CREATED_VIA ); self::set_final_status( $order, $status ); $order->set_date_created( $date ); if ( in_array( $status, Plan::PAID, true ) ) { $order->set_date_paid( min( $ctx->to, $date + 60 * (int) ( $plan['paid_minutes'] ?? 5 ) ) ); } if ( in_array( $status, array( 'completed', 'refunded' ), true ) ) { $order->set_date_completed( min( $ctx->to, $date + DAY_IN_SECONDS * (int) ( $plan['done_days'] ?? 1 ) ) ); } $order->set_currency( get_woocommerce_currency() ); $order->set_prices_include_tax( wc_prices_include_tax() ); $order->set_customer_ip_address( Words::ip( $random ) ); $order->set_customer_user_agent( 'Admin Powerups test data' ); $payment = in_array( $plan['payment'] ?? '', array( 'bacs', 'cheque', 'cod' ), true ) ? (string) $plan['payment'] : 'bacs'; $order->set_payment_method( $payment ); $order->set_payment_method_title( self::gateway_title( $payment ) ); if ( $customer ) { $order->set_customer_id( $customer->get_id() ); foreach ( array( 'first_name', 'last_name', 'company', 'address_1', 'city', 'state', 'postcode', 'country', 'email', 'phone' ) as $field ) { $order->{"set_billing_{$field}"}( (string) $customer->{"get_billing_{$field}"}() ); } foreach ( array( 'first_name', 'last_name', 'company', 'address_1', 'city', 'state', 'postcode', 'country' ) as $field ) { $order->{"set_shipping_{$field}"}( (string) $customer->{"get_shipping_{$field}"}() ); } if ( '' === $order->get_billing_email() ) { $order->set_billing_email( $customer->get_email() ); } } else { $first = (string) $random->pick( $ctx->words['first_names'] ); $last = (string) $random->pick( $ctx->words['last_names'] ); $address = Customers::address( $random, $ctx, $random->chance( 70 ) ); $order->set_billing_first_name( $first ); $order->set_billing_last_name( $last ); $order->set_billing_email( 'guest.' . Words::ascii( $first ) . '.' . $random->int( 10000, 99999 ) . '@' . $ctx->domain ); foreach ( array( 'address_1', 'city', 'state', 'postcode', 'country', 'phone' ) as $field ) { $order->{"set_billing_{$field}"}( $address[ $field ] ); } $order->set_shipping_first_name( $first ); $order->set_shipping_last_name( $last ); foreach ( array( 'address_1', 'city', 'state', 'postcode', 'country' ) as $field ) { $order->{"set_shipping_{$field}"}( $address[ $field ] ); } } $physical = false; $lines = 0; foreach ( (array) ( $plan['items'] ?? array() ) as $line ) { $product = $ctx->product( $products[ max( 0, min( count( $products ) - 1, (int) ( $line['product'] ?? 0 ) ) ) ] ); if ( $product && $product->is_type( 'variable' ) ) { $children = array_values( $product->get_children() ); $product = $children ? $ctx->product( (int) $children[ (int) floor( max( 0, min( 0.9999, (float) ( $line['variation'] ?? 0 ) ) ) * count( $children ) ) ] ) : null; } if ( ! $product ) { continue; } try { $order->add_product( $product, max( 1, min( 10, (int) ( $line['qty'] ?? 1 ) ) ) ); } catch ( \Exception $e ) { continue; } ++$lines; $physical = $physical || $product->needs_shipping(); } if ( ! $lines ) { return $none; } if ( $physical ) { $shipping = new \WC_Order_Item_Shipping(); if ( ! empty( $plan['free_shipping'] ) ) { $shipping->set_method_id( 'free_shipping' ); $shipping->set_method_title( __( 'Free shipping', 'admin-powerups-for-woocommerce' ) ); $shipping->set_total( '0' ); } else { $shipping->set_method_id( 'flat_rate' ); $shipping->set_method_title( __( 'Flat rate', 'admin-powerups-for-woocommerce' ) ); $shipping->set_total( wc_format_decimal( max( 0, (int) ( $plan['shipping'] ?? 5 ) ) ) ); } $order->add_item( $shipping ); } // calculate_totals() ends with the first save: marker, status and dates go in with it (an // explicit save() first would cost a second full save per order). try { $order->calculate_totals( wc_tax_enabled() ); } catch ( \Throwable $e ) { // Inserted before the error (a plugin on `woocommerce_new_order`, say): sign it, so // Clean up can remove it. if ( $order->get_id() && (string) $order->get_meta( Marker::META ) === $ctx->batch ) { Marker::sign_order( $order, $ctx->batch ); } throw $e; } $id = (int) $order->get_id(); if ( ! $id ) { return $none; } Marker::sign_order( $order, $ctx->batch ); $coupon_index = (int) ( $plan['coupon'] ?? -1 ); if ( $coupon_index >= 0 && $coupons ) { $coupon_id = $coupons[ min( count( $coupons ) - 1, $coupon_index ) ]; $code = wc_get_coupon_code_by_id( $coupon_id ); // apply_coupon() recalculates the totals and saves. $applied = $code ? $order->apply_coupon( $code ) : false; // It counted one use unless the order is cancelled or failed. if ( true === $applied && ! in_array( $status, array( 'cancelled', 'failed' ), true ) ) { Counters::add( $coupon_id, Marker::USAGE, 1 ); } } if ( in_array( $status, Plan::SALES, true ) ) { // Counters::recorded_sales() (woocommerce_recorded_sales) adds these to our count. wc_update_total_sales_counts( $id ); } if ( $customer && in_array( $status, Plan::PAID, true ) && ! $customer->get_is_paying_customer() ) { $customer->set_is_paying_customer( true ); $customer->save(); } $refunds = 0; if ( in_array( $plan['refund'] ?? '', array( 'full', 'partial' ), true ) ) { $total = (float) $order->get_total(); $amount = 'full' === $plan['refund'] ? $total : round( $total * max( 1, min( 99, (int) ( $plan['refund_share'] ?? 20 ) ) ) / 100, max( 0, wc_get_price_decimals() ) ); // A "partial" refund that rounds up to the whole total would change the status. if ( $amount > 0 && ( 'full' === $plan['refund'] || $amount < $total ) ) { self::$refund = array( $ctx->batch, min( $ctx->to, $date + DAY_IN_SECONDS * max( 1, (int) ( $plan['refund_days'] ?? 1 ) ) ) ); add_action( 'woocommerce_create_refund', array( __CLASS__, 'mark_refund' ) ); // The final status is already set: WooCommerce mustn't change it after a full refund. add_filter( 'woocommerce_order_fully_refunded_status', array( Guard::class, 'off' ), PHP_INT_MAX ); try { $refund = wc_create_refund( array( 'amount' => wc_format_decimal( $amount ), 'reason' => __( 'Test data', 'admin-powerups-for-woocommerce' ), 'order_id' => $id, 'refund_payment' => false, 'restock_items' => false, ) ); } catch ( \Exception $e ) { $refund = null; } finally { remove_action( 'woocommerce_create_refund', array( __CLASS__, 'mark_refund' ) ); remove_filter( 'woocommerce_order_fully_refunded_status', array( Guard::class, 'off' ), PHP_INT_MAX ); self::$refund = array( '', 0 ); } if ( $refund instanceof \WC_Order_Refund ) { Marker::sign_order( $refund, $ctx->batch ); ++$refunds; $ctx->created( 'refund', $refund->get_id() ); } } } self::queue_import( $id ); $ctx->created( 'order', $id ); return array( 'id' => $id, 'refunds' => $refunds, ); } /** * Queue an order's Analytics import (WooCommerce queues one only on later updates). * * WooCommerce 10.2+ listens to `woocommerce_schedule_import` while Analytics imports right * away. Before 10.2 nothing listens to it, so when the orders scheduler is hooked for * immediate imports (its `woocommerce_create_order` filter) we call it directly. A no-op when * Analytics is off or imports on a schedule. * * @since 1.0.0 * @param int $id Order ID. * @return void */ public static function queue_import( int $id ): void { $callback = array( 'Automattic\\WooCommerce\\Internal\\Admin\\Schedulers\\OrdersScheduler', 'possibly_schedule_import' ); if ( false === has_action( 'woocommerce_schedule_import', $callback ) && is_callable( $callback ) && false !== has_filter( 'woocommerce_create_order', $callback ) ) { call_user_func( $callback, $id ); return; } do_action( 'woocommerce_schedule_import', $id ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce's own hook. } /** * `woocommerce_create_refund` (only while our refund is made): marker and date before its save. * * @since 1.0.0 * @param mixed $refund Refund. * @return void */ public static function mark_refund( $refund ): void { if ( $refund instanceof \WC_Order_Refund && '' !== self::$refund[0] ) { $refund->update_meta_data( Marker::META, self::$refund[0] ); if ( self::$refund[1] ) { $refund->set_date_created( self::$refund[1] ); } } } /** * Give a new order its final status without recording a status change. * * A new order's status reads as "pending" (WC_Abstract_Order::get_status(), view context), so * set_status() would record pending → X and the save would fire every status hook (emails, * stock, payment_complete, other plugins). WC_Order::set_status() records a transition only * while the object is "read" (class-wc-order.php, `true === $this->object_read`), so the status * is set with that flag off. WooCommerce's valid-status check is skipped with it: callers pass * a core status (Plan::STATUSES). Date paid / completed are set by the caller. * * @param \WC_Order $order New, unsaved order. * @param string $status Core status without `wc-`. * @return void */ private static function set_final_status( \WC_Order $order, string $status ): void { $order->set_object_read( false ); $order->set_status( $status ); $order->set_object_read( true ); } /** * The title of a payment gateway, without loading the gateways. * * @param string $id Gateway ID. * @return string */ private static function gateway_title( string $id ): string { $settings = get_option( 'woocommerce_' . $id . '_settings' ); if ( is_array( $settings ) && ! empty( $settings['title'] ) && is_string( $settings['title'] ) ) { return sanitize_text_field( $settings['title'] ); } $titles = array( 'bacs' => __( 'Direct bank transfer', 'admin-powerups-for-woocommerce' ), 'cheque' => __( 'Check payments', 'admin-powerups-for-woocommerce' ), 'cod' => __( 'Cash on delivery', 'admin-powerups-for-woocommerce' ), ); return $titles[ $id ] ?? $id; } }