diff --git a/includes/class-woocommerce.php b/includes/class-woocommerce.php index 359e333e..b1e4e2cd 100644 --- a/includes/class-woocommerce.php +++ b/includes/class-woocommerce.php @@ -47,6 +47,41 @@ class WooCommerce { // WPGraphQL Reset password -> Use woocommerce email password template when requested. add_filter( 'retrieve_password_message', [ self::class, 'get_reset_password_message' ], 10, 3 ); add_filter( 'retrieve_password_title', [ self::class, 'get_reset_password_title' ] ); + + // Brand the WooCommerce Order Attribution "Origin" for orders created through WPGraphQL. + add_filter( 'wc_order_attribution_origin_label', [ self::class, 'order_attribution_origin_label' ], 10, 4 ); + } + + /** + * Returns the WooCommerce Order Attribution source type used to mark orders created through WPGraphQL. + * + * Matches the default `created_via` value so GraphQL-created orders are attributable out of the box. + * + * @return string + */ + public static function get_order_attribution_source_type() { + return apply_filters( 'graphql_woocommerce_order_attribution_source_type', 'graphql-api' ); + } + + /** + * Provides the WooCommerce Order Attribution "Origin" label for orders created through WPGraphQL. + * + * Connected to WooCommerce's order origin label filter so orders tagged with our attribution + * source type surface a recognizable origin instead of "Unknown". + * + * @param string $label Origin label. May contain a "%s" placeholder for the source. + * @param string $source_type Attribution source type. + * @param string $source Attribution source. + * @param string $formatted_source Formatted attribution source. + * + * @return string + */ + public static function order_attribution_origin_label( $label, $source_type, $source, $formatted_source ) { + if ( self::get_order_attribution_source_type() !== $source_type ) { + return $label; + } + + return apply_filters( 'graphql_woocommerce_order_attribution_origin_label', __( 'GraphQL', 'wp-graphql-woocommerce' ), $source, $formatted_source ); } /** diff --git a/includes/data/mutation/class-checkout-mutation.php b/includes/data/mutation/class-checkout-mutation.php index 16ce19a9..13e33c86 100644 --- a/includes/data/mutation/class-checkout-mutation.php +++ b/includes/data/mutation/class-checkout-mutation.php @@ -649,6 +649,13 @@ class Checkout_Mutation { throw new UserError( __( 'Unable to create order.', 'wp-graphql-woocommerce' ) ); } + // Override the "created via" source when provided. WC_Checkout::create_order() hardcodes it to "checkout". + if ( ! empty( $input['createdVia'] ) ) { + $order->set_created_via( $input['createdVia'] ); + $order->add_meta_data( '_wc_order_attribution_source_type', $input['createdVia'], true ); + $order->save(); + } + // Add meta data. if ( ! empty( $input['metaData'] ) ) { self::update_order_meta( $order_id, $input['metaData'], $input, $context, $info ); diff --git a/includes/mutation/class-checkout.php b/includes/mutation/class-checkout.php index 93fd746a..3ccc2bba 100644 --- a/includes/mutation/class-checkout.php +++ b/includes/mutation/class-checkout.php @@ -111,6 +111,12 @@ class Checkout { return __( 'Fees to add to the order.', 'wp-graphql-woocommerce' ); }, ], + 'createdVia' => [ + 'type' => 'String', + 'description' => static function () { + return __( 'Source of the order. Useful when WooCommerce is driven from multiple sources. Defaults to "checkout".', 'wp-graphql-woocommerce' ); + }, + ], ]; } diff --git a/includes/mutation/class-order-create.php b/includes/mutation/class-order-create.php index 9acfa403..d3c54c4c 100644 --- a/includes/mutation/class-order-create.php +++ b/includes/mutation/class-order-create.php @@ -16,6 +16,7 @@ use WC_Order_Factory; use WPGraphQL\AppContext; use WPGraphQL\WooCommerce\Data\Mutation\Order_Mutation; use WPGraphQL\WooCommerce\Model\Order; +use WPGraphQL\WooCommerce\WooCommerce; /** * Class Order_Create @@ -140,6 +141,12 @@ class Order_Create { return __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'wp-graphql-woocommerce' ); }, ], + 'createdVia' => [ + 'type' => 'String', + 'description' => static function () { + return __( 'Source of the order. Useful when WooCommerce is driven from multiple sources. Defaults to "graphql-api".', 'wp-graphql-woocommerce' ); + }, + ], ]; } @@ -203,7 +210,9 @@ class Order_Create { Order_Mutation::apply_coupons( $order, $input['coupons'] ); } - $order->set_created_via( 'graphql-api' ); + $created_via = ! empty( $input['createdVia'] ) ? $input['createdVia'] : WooCommerce::get_order_attribution_source_type(); + $order->set_created_via( $created_via ); + $order->add_meta_data( '_wc_order_attribution_source_type', $created_via, true ); $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); $order->calculate_totals( true ); diff --git a/tests/wpunit/CheckoutMutationTest.php b/tests/wpunit/CheckoutMutationTest.php index aaa1a5e6..a72535ca 100644 --- a/tests/wpunit/CheckoutMutationTest.php +++ b/tests/wpunit/CheckoutMutationTest.php @@ -505,6 +505,27 @@ class CheckoutMutationTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGrap $this->assertEquals( '555-555-6789', $order->get_shipping_phone() ); } + public function testCheckoutMutationWithCreatedVia() { + $this->loginAsCustomer(); + + $product_id = $this->factory->product->createSimple(); + WC()->cart->add_to_cart( $product_id, 1 ); + + $variables = [ 'input' => $this->getCheckoutInput( [ 'createdVia' => 'pos' ] ) ]; + $query = $this->getCheckoutMutation(); + + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQuerySuccessful( + $response, + [ $this->expectedField( 'checkout.order.createdVia', 'pos' ) ] + ); + + // The provided "createdVia" overrides WooCommerce's hardcoded "checkout" source and tags the attribution source type. + $order = \wc_get_order( $response['data']['checkout']['order']['databaseId'] ); + $this->assertEquals( 'pos', $order->get_created_via() ); + $this->assertEquals( 'pos', $order->get_meta( '_wc_order_attribution_source_type' ) ); + } + public function testCheckoutMutationWithNewAccount() { $variable = $this->factory->product_variation->createSome(); $product_ids = [ diff --git a/tests/wpunit/OrderMutationsTest.php b/tests/wpunit/OrderMutationsTest.php index 30eb7138..7f875354 100644 --- a/tests/wpunit/OrderMutationsTest.php +++ b/tests/wpunit/OrderMutationsTest.php @@ -351,8 +351,12 @@ class OrderMutationsTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQ $this->expectedField( 'createOrder.order.shipping.country', 'US' ), $this->expectedField( 'createOrder.order.paymentMethod', 'bacs' ), $this->expectedField( 'createOrder.order.paymentMethodTitle', 'Direct Bank Transfer' ), + $this->expectedField( 'createOrder.order.createdVia', 'graphql-api' ), ]; + // The order is attributed to the "graphql-api" source so WooCommerce surfaces the "GraphQL" origin. + $this->assertEquals( 'graphql-api', $order->get_meta( '_wc_order_attribution_source_type' ) ); + // Validate coupon lines. $coupon_items = array_values( $order->get_items( 'coupon' ) ); foreach ( $coupon_items as $i => $item ) { @@ -412,6 +416,38 @@ class OrderMutationsTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQ $this->assertQuerySuccessful( $response, $expected ); } + public function testCreateOrderMutationWithCreatedVia() { + $this->loginAsShopManager(); + + $product_id = $this->factory->product->createSimple(); + $input = [ + 'createdVia' => 'pos', + 'lineItems' => [ + [ + 'productId' => $product_id, + 'quantity' => 1, + ], + ], + ]; + + $response = $this->orderMutation( $input ); + $this->assertQuerySuccessful( + $response, + [ $this->expectedField( 'createOrder.order.createdVia', 'pos' ) ] + ); + + // A provided "createdVia" flows into both the order source and the WooCommerce attribution source type. + $order = \WC_Order_Factory::get_order( $response['data']['createOrder']['order']['databaseId'] ); + $this->assertEquals( 'pos', $order->get_created_via() ); + $this->assertEquals( 'pos', $order->get_meta( '_wc_order_attribution_source_type' ) ); + + // Orders attributed to "graphql-api" surface a branded "GraphQL" origin in WooCommerce. + $this->assertEquals( + 'GraphQL', + apply_filters( 'wc_order_attribution_origin_label', 'Unknown', 'graphql-api', '', 'Unknown' ) + ); + } + public function testUpdateOrderMutation() { // Create products and coupons to be used in order creation. $variable = $this->factory->product_variation->createSome( $this->factory->product->createVariable() );