Files
2ce9424e11 feat: configurable createdVia on order mutations + WooCommerce order attribution (#1018)
* Add option to Change the created_via field.

Can be useful when WooCommerce is being used from multiply sources. For example, with plugins like Point of Sale for WooCommerce.

* feat: createdVia on checkout + WooCommerce order attribution origin

Builds on the createOrder createdVia option:

- Add a createdVia input to the checkout mutation. WC_Checkout::create_order()
  hardcodes created_via to 'checkout' after its data loop, so process_checkout()
  overrides it (and tags the attribution source type) only when createdVia is
  provided; otherwise checkout keeps WooCommerce's 'checkout' default.
- createOrder now defaults created_via to WooCommerce::get_order_attribution_source_type()
  ('graphql-api', filterable) and writes that value to the
  _wc_order_attribution_source_type order meta so GraphQL orders are attributable.
- Register a wc_order_attribution_origin_label callback that brands orders
  attributed to 'graphql-api' as the 'GraphQL' origin in WooCommerce admin.
- Cover createdVia + attribution in OrderMutationsTest and CheckoutMutationTest.

---------

Co-authored-by: Scott Kennedy <scottyzen@gmail.com>
2026-06-12 13:16:53 -04:00

1266 lines
38 KiB
PHP

<?php
class CheckoutMutationTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQLTestCase {
public function setUp(): void {
// before
parent::setUp();
// Force WP_Filesystem to use the direct method so WooCommerce's
// FileV2 log handler doesn't attempt FTP operations which fail
// in the test environment (null FTP connection on PHP 8.1+).
add_filter( 'filesystem_method', function () {
return 'direct';
} );
$this->loginAs( 0 );
// Turn on tax calculations and store shipping countries. Important!
update_option( 'woocommerce_ship_to_countries', 'all' );
update_option( 'woocommerce_prices_include_tax', 'no' );
update_option( 'woocommerce_calc_taxes', 'yes' );
update_option( 'woocommerce_tax_round_at_subtotal', 'no' );
// Turn on guest checkout.
update_option( 'woocommerce_enable_guest_checkout', 'yes' );
// Enable payment gateways.
$gateways = \WC()->payment_gateways->payment_gateways();
$bacs_gateway = $gateways['bacs'];
$bacs_gateway->settings['enabled'] = 'yes';
update_option( $bacs_gateway->get_option_key(), $bacs_gateway->settings );
$stripe_settings = WC_Stripe_Helper::get_stripe_settings();
$stripe_settings['enabled'] = 'yes';
$stripe_settings['testmode'] = 'yes';
$stripe_settings['test_publishable_key'] = defined( 'STRIPE_API_PUBLISHABLE_KEY' )
? STRIPE_API_PUBLISHABLE_KEY
: getenv( 'STRIPE_API_PUBLISHABLE_KEY' );
$stripe_settings['test_secret_key'] = defined( 'STRIPE_API_SECRET_KEY' )
? STRIPE_API_SECRET_KEY
: getenv( 'STRIPE_API_SECRET_KEY' );
WC_Stripe_Helper::update_main_stripe_settings( $stripe_settings );
$_SERVER['HTTPS'] = false;
add_filter( 'wc_stripe_is_upe_checkout_enabled', '__return_false' );
add_filter(
'woocommerce_available_payment_gateways',
function( $available_gateways ) {
$stripe_gateway = new WC_Gateway_Stripe();
$available_gateways[ $stripe_gateway->id ] = $stripe_gateway;
return $available_gateways;
}
);
\WC()->payment_gateways->init();
// Additional cart fees.
add_action(
'woocommerce_cart_calculate_fees',
static function () {
$percentage = 0.01;
$surcharge = ( WC()->cart->cart_contents_total + WC()->cart->shipping_total ) * $percentage;
WC()->cart->add_fee( 'Surcharge', $surcharge, true, '' );
}
);
// Create a tax rate.
$this->factory->tax_rate->create(
[
'country' => '',
'state' => '',
'rate' => 20.000,
'name' => 'VAT',
'priority' => '1',
'compound' => '0',
'shipping' => '1',
'class' => '',
]
);
}
private function getCheckoutMutation() {
return '
mutation checkout( $input: CheckoutInput! ) {
checkout( input: $input ) {
clientMutationId
order {
id
databaseId
currency
orderVersion
date
modified
status
discountTotal
discountTax
shippingTotal
shippingTax
cartTax
total
totalTax
subtotal
orderNumber
orderKey
createdVia
pricesIncludeTax
parent {
id
}
customer {
id
}
customerIpAddress
customerUserAgent
customerNote
billing {
firstName
lastName
company
address1
address2
city
state
postcode
country
email
phone
}
shipping {
firstName
lastName
company
address1
address2
city
state
postcode
country
phone
}
paymentMethod
paymentMethodTitle
transactionId
dateCompleted
datePaid
cartHash
shippingAddressMapUrl
hasBillingAddress
hasShippingAddress
isDownloadPermitted
needsShippingAddress
hasDownloadableItem
downloadableItems {
nodes {
url
accessExpires
downloadId
downloadsRemaining
name
product {
databaseId
}
download {
downloadId
}
}
}
needsPayment
needsProcessing
metaData {
key
value
}
couponLines {
nodes {
databaseId
orderId
code
discount
discountTax
coupon {
id
}
}
}
feeLines {
nodes {
databaseId
orderId
amount
name
taxStatus
total
totalTax
taxClass
}
}
shippingLines {
nodes {
databaseId
orderId
methodTitle
total
totalTax
taxClass
}
}
taxLines {
nodes {
rateCode
label
taxTotal
shippingTaxTotal
isCompound
taxRate {
databaseId
}
}
}
lineItems {
nodes {
productId
variationId
quantity
taxClass
subtotal
subtotalTax
total
totalTax
taxStatus
product {
node {
... on SimpleProduct {
id
}
... on VariableProduct {
id
}
}
}
variation {
node {
id
}
}
}
}
}
customer {
id
}
result
redirect
}
}
';
}
private function getCartQuery() {
return '
query {
cart {
contents {
nodes {
key
}
}
total
}
}
';
}
private function getCheckoutInput( $overwrite = [] ) {
return array_merge(
[
'paymentMethod' => 'bacs',
'shippingMethod' => [ 'flat rate' ],
'customerNote' => 'Test customer note',
'billing' => [
'firstName' => 'May',
'lastName' => 'Parker',
'address1' => '20 Ingram St',
'city' => 'New York City',
'state' => 'NY',
'postcode' => '12345',
'country' => 'US',
'email' => 'superfreak500@gmail.com',
'phone' => '555-555-1234',
'overwrite' => true,
],
'shipping' => [
'firstName' => 'May',
'lastName' => 'Parker',
'address1' => '20 Ingram St',
'city' => 'New York City',
'state' => 'NY',
'postcode' => '12345',
'country' => 'US',
'phone' => '555-555-6789',
],
'metaData' => [
[
'key' => 'test_key',
'value' => 'test value',
],
],
],
$overwrite
);
}
// tests
public function testCheckoutMutation() {
$this->loginAsCustomer();
WC()->customer->set_billing_company( 'Harris Teeter' );
WC()->customer->save();
$variable = $this->factory->product_variation->createSome();
$product_ids = [
$this->factory->product->createSimple(),
$this->factory->product->createSimple(),
$variable['product'],
];
$coupon = new WC_Coupon(
$this->factory->coupon->create(
[ 'product_ids' => $product_ids ]
)
);
WC()->cart->add_to_cart( $product_ids[0], 3 );
WC()->cart->add_to_cart( $product_ids[1], 6 );
WC()->cart->add_to_cart(
$product_ids[2],
2,
$variable['variations'][0],
[ 'attribute_pa_color' => 'red' ]
);
WC()->cart->apply_coupon( $coupon->get_code() );
$variables = [ 'input' => $this->getCheckoutInput() ];
$query = $this->getCheckoutMutation();
/**
* Assertion One
*
* Test mutation and input.
*/
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedField( 'checkout.order.id', static::NOT_NULL ),
$this->expectedField( 'checkout.order.status', 'ON_HOLD' ),
$this->expectedNode(
'checkout.order.metaData',
[
$this->expectedField( 'key', 'test_key' ),
$this->expectedField( 'value', 'test value' ),
]
),
$this->expectedNode(
'checkout.order.couponLines.nodes',
[
$this->expectedField( 'code', $coupon->get_code() ),
$this->expectedField( 'databaseId', static::NOT_NULL ),
$this->expectedField( 'orderId', static::NOT_NULL ),
$this->expectedField( 'discount', static::NOT_NULL ),
$this->expectedField( 'discountTax', static::NOT_NULL ),
$this->expectedField( 'coupon', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.feeLines.nodes',
[
$this->expectedField( 'name', 'Surcharge' ),
$this->expectedField( 'databaseId', static::NOT_NULL ),
$this->expectedField( 'orderId', static::NOT_NULL ),
$this->expectedField( 'amount', static::NOT_NULL ),
$this->expectedField( 'taxStatus', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.shippingLines.nodes',
[
$this->expectedField( 'methodTitle', 'Flat rate' ),
$this->expectedField( 'databaseId', static::NOT_NULL ),
$this->expectedField( 'orderId', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.taxLines.nodes',
[
$this->expectedField( 'label', 'VAT' ),
$this->expectedField( 'rateCode', static::NOT_NULL ),
$this->expectedField( 'taxTotal', static::NOT_NULL ),
$this->expectedField( 'shippingTaxTotal', static::NOT_NULL ),
$this->expectedField( 'isCompound', static::NOT_NULL ),
$this->expectedField( 'taxRate', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.lineItems.nodes',
[
$this->expectedField( 'productId', $product_ids[0] ),
$this->expectedField( 'quantity', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
$this->expectedField( 'subtotal', static::NOT_NULL ),
$this->expectedField( 'subtotalTax', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxStatus', static::NOT_NULL ),
$this->expectedField( 'product.node.id', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.lineItems.nodes',
[
$this->expectedField( 'productId', $product_ids[1] ),
$this->expectedField( 'quantity', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
$this->expectedField( 'subtotal', static::NOT_NULL ),
$this->expectedField( 'subtotalTax', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxStatus', static::NOT_NULL ),
$this->expectedField( 'product.node.id', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.lineItems.nodes',
[
$this->expectedField( 'productId', $product_ids[2] ),
$this->expectedField( 'variationId', static::NOT_NULL ),
$this->expectedField( 'quantity', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
$this->expectedField( 'subtotal', static::NOT_NULL ),
$this->expectedField( 'subtotalTax', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxStatus', static::NOT_NULL ),
$this->expectedField( 'product.node.id', static::NOT_NULL ),
$this->expectedField( 'variation.node.id', static::NOT_NULL ),
]
),
$this->expectedField(
'checkout.customer.id',
$this->toRelayId( 'user', $this->customer )
),
$this->expectedField( 'checkout.order.billing.phone', '555-555-1234' ),
$this->expectedField( 'checkout.order.shipping.phone', '555-555-6789' ),
$this->expectedField( 'checkout.result', 'success' ),
$this->expectedField( 'checkout.redirect', static::NOT_NULL ),
];
$this->assertQuerySuccessful( $response, $expected );
// Confirm cart empty after successful checkout.
$query = $this->getCartQuery();
$response = $this->graphql( compact( 'query' ) );
$expected = [
$this->expectedField( 'cart.contents.nodes', [] ),
$this->expectedField( 'cart.total', '$0.00' ),
];
$this->assertQuerySuccessful( $response, $expected );
}
public function testCheckoutMutationPersistsShippingPhone() {
$this->loginAsCustomer();
$product_id = $this->factory->product->createSimple();
WC()->cart->add_to_cart( $product_id, 1 );
$input = [
'shipping' => [
'firstName' => 'May',
'lastName' => 'Parker',
'address1' => '20 Ingram St',
'city' => 'New York City',
'state' => 'NY',
'postcode' => '12345',
'country' => 'US',
'phone' => '555-555-6789',
],
];
$variables = [ 'input' => $this->getCheckoutInput( $input ) ];
$query = $this->getCheckoutMutation();
$response = $this->graphql( compact( 'query', 'variables' ) );
// The shipping phone is returned on the order and is distinct from the billing phone.
$this->assertQuerySuccessful(
$response,
[
$this->expectedField( 'checkout.order.billing.phone', '555-555-1234' ),
$this->expectedField( 'checkout.order.shipping.phone', '555-555-6789' ),
]
);
// And it is saved on the underlying WC_Order object.
$order_id = $response['data']['checkout']['order']['databaseId'];
$order = \wc_get_order( $order_id );
$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 = [
$this->factory->product->createSimple(),
$this->factory->product->createSimple(),
$variable['product'],
];
$coupon = new WC_Coupon(
$this->factory->coupon->create(
[ 'product_ids' => $product_ids ]
)
);
WC()->cart->add_to_cart( $product_ids[0], 3 );
WC()->cart->add_to_cart( $product_ids[1], 6 );
WC()->cart->add_to_cart(
$product_ids[2],
2,
$variable['variations'][0],
[ 'attribute_pa_color' => 'red' ]
);
WC()->cart->apply_coupon( $coupon->get_code() );
$input = [
'account' => [
'username' => 'test_user_1',
'password' => 'test_pass',
],
];
$variables = [ 'input' => $this->getCheckoutInput( $input ) ];
$query = $this->getCheckoutMutation();
/**
* Assertion One
*
* Test mutation and input.
*/
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedField( 'checkout.order.id', static::NOT_NULL ),
$this->expectedField( 'checkout.order.status', 'ON_HOLD' ),
$this->expectedNode(
'checkout.order.metaData',
[
$this->expectedField( 'key', 'test_key' ),
$this->expectedField( 'value', 'test value' ),
]
),
$this->expectedNode(
'checkout.order.feeLines.nodes',
[
$this->expectedField( 'name', 'Surcharge' ),
$this->expectedField( 'databaseId', static::NOT_NULL ),
$this->expectedField( 'orderId', static::NOT_NULL ),
$this->expectedField( 'amount', static::NOT_NULL ),
$this->expectedField( 'taxStatus', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.shippingLines.nodes',
[
$this->expectedField( 'methodTitle', 'Flat rate' ),
$this->expectedField( 'databaseId', static::NOT_NULL ),
$this->expectedField( 'orderId', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.taxLines.nodes',
[
$this->expectedField( 'label', 'VAT' ),
$this->expectedField( 'rateCode', static::NOT_NULL ),
$this->expectedField( 'taxTotal', static::NOT_NULL ),
$this->expectedField( 'shippingTaxTotal', static::NOT_NULL ),
$this->expectedField( 'isCompound', static::NOT_NULL ),
$this->expectedField( 'taxRate', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.lineItems.nodes',
[
$this->expectedField( 'productId', $product_ids[0] ),
$this->expectedField( 'quantity', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
$this->expectedField( 'subtotal', static::NOT_NULL ),
$this->expectedField( 'subtotalTax', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxStatus', static::NOT_NULL ),
$this->expectedField( 'product.node.id', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.lineItems.nodes',
[
$this->expectedField( 'productId', $product_ids[1] ),
$this->expectedField( 'quantity', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
$this->expectedField( 'subtotal', static::NOT_NULL ),
$this->expectedField( 'subtotalTax', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxStatus', static::NOT_NULL ),
$this->expectedField( 'product.node.id', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.lineItems.nodes',
[
$this->expectedField( 'productId', $product_ids[2] ),
$this->expectedField( 'variationId', static::NOT_NULL ),
$this->expectedField( 'quantity', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
$this->expectedField( 'subtotal', static::NOT_NULL ),
$this->expectedField( 'subtotalTax', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxStatus', static::NOT_NULL ),
$this->expectedField( 'product.node.id', static::NOT_NULL ),
$this->expectedField( 'variation.node.id', static::NOT_NULL ),
]
),
$this->expectedField( 'checkout.customer.id', static::NOT_NULL ),
$this->expectedField( 'checkout.result', 'success' ),
$this->expectedField( 'checkout.redirect', static::NOT_NULL ),
];
$this->assertQuerySuccessful( $response, $expected );
// Confirm cart empty after successful checkout.
$query = $this->getCartQuery();
$response = $this->graphql( compact( 'query' ) );
$expected = [
$this->expectedField( 'cart.contents.nodes', [] ),
$this->expectedField( 'cart.total', '$0.00' ),
];
$this->assertQuerySuccessful( $response, $expected );
}
public function testCheckoutMutationWithNoAccount() {
WC()->customer->set_billing_email( 'superfreak500@gmail.com' );
WC()->customer->save();
$variable = $this->factory->product_variation->createSome();
$product_ids = [
$this->factory->product->createSimple(),
$this->factory->product->createSimple(),
$variable['product'],
];
$coupon = new WC_Coupon(
$this->factory->coupon->create(
[ 'product_ids' => $product_ids ]
)
);
WC()->cart->add_to_cart( $product_ids[0], 3 );
WC()->cart->add_to_cart( $product_ids[1], 6 );
WC()->cart->add_to_cart(
$product_ids[2],
2,
$variable['variations'][0],
[ 'attribute_pa_color' => 'red' ]
);
WC()->cart->apply_coupon( $coupon->get_code() );
$variables = [ 'input' => $this->getCheckoutInput() ];
$query = $this->getCheckoutMutation();
/**
* Assertion One
*
* Test mutation and input.
*/
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedField( 'checkout.order.id', static::NOT_NULL ),
$this->expectedField( 'checkout.order.status', 'ON_HOLD' ),
$this->expectedNode(
'checkout.order.metaData',
[
$this->expectedField( 'key', 'test_key' ),
$this->expectedField( 'value', 'test value' ),
]
),
$this->expectedNode(
'checkout.order.feeLines.nodes',
[
$this->expectedField( 'name', 'Surcharge' ),
$this->expectedField( 'databaseId', static::NOT_NULL ),
$this->expectedField( 'orderId', static::NOT_NULL ),
$this->expectedField( 'amount', static::NOT_NULL ),
$this->expectedField( 'taxStatus', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.shippingLines.nodes',
[
$this->expectedField( 'methodTitle', 'Flat rate' ),
$this->expectedField( 'databaseId', static::NOT_NULL ),
$this->expectedField( 'orderId', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.taxLines.nodes',
[
$this->expectedField( 'label', 'VAT' ),
$this->expectedField( 'rateCode', static::NOT_NULL ),
$this->expectedField( 'taxTotal', static::NOT_NULL ),
$this->expectedField( 'shippingTaxTotal', static::NOT_NULL ),
$this->expectedField( 'isCompound', static::NOT_NULL ),
$this->expectedField( 'taxRate', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.lineItems.nodes',
[
$this->expectedField( 'productId', $product_ids[0] ),
$this->expectedField( 'quantity', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
$this->expectedField( 'subtotal', static::NOT_NULL ),
$this->expectedField( 'subtotalTax', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxStatus', static::NOT_NULL ),
$this->expectedField( 'product.node.id', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.lineItems.nodes',
[
$this->expectedField( 'productId', $product_ids[1] ),
$this->expectedField( 'quantity', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
$this->expectedField( 'subtotal', static::NOT_NULL ),
$this->expectedField( 'subtotalTax', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxStatus', static::NOT_NULL ),
$this->expectedField( 'product.node.id', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.lineItems.nodes',
[
$this->expectedField( 'productId', $product_ids[2] ),
$this->expectedField( 'variationId', static::NOT_NULL ),
$this->expectedField( 'quantity', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
$this->expectedField( 'subtotal', static::NOT_NULL ),
$this->expectedField( 'subtotalTax', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxStatus', static::NOT_NULL ),
$this->expectedField( 'product.node.id', static::NOT_NULL ),
$this->expectedField( 'variation.node.id', static::NOT_NULL ),
]
),
$this->expectedField( 'checkout.customer.id', 'guest' ),
$this->expectedField( 'checkout.result', 'success' ),
$this->expectedField( 'checkout.redirect', static::NOT_NULL ),
];
$this->assertQuerySuccessful( $response, $expected );
// Confirm cart empty after successful checkout.
$query = $this->getCartQuery();
$response = $this->graphql( compact( 'query' ) );
$expected = [
$this->expectedField( 'cart.contents.nodes', [] ),
$this->expectedField( 'cart.total', '$0.00' ),
];
$this->assertQuerySuccessful( $response, $expected );
}
public function testCheckoutMutationWithPrepaidOrder() {
WC()->customer->set_billing_email( 'superfreak500@gmail.com' );
WC()->customer->save();
$product_ids = [
$this->factory->product->createSimple(
[
'virtual' => true,
'downloadable' => true,
]
),
$this->factory->product->createSimple(
[
'virtual' => true,
'downloadable' => true,
]
),
];
$coupon = new WC_Coupon(
$this->factory->coupon->create( [ 'product_ids' => $product_ids ] )
);
WC()->cart->add_to_cart( $product_ids[0], 3 );
WC()->cart->add_to_cart( $product_ids[1], 6 );
WC()->cart->apply_coupon( $coupon->get_code() );
$input = [
'isPaid' => true,
'transactionId' => 'transaction_id',
];
$variables = [ 'input' => $this->getCheckoutInput( $input ) ];
$query = $this->getCheckoutMutation();
/**
* Assertion One
*
* Test mutation and input.
*/
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedField( 'checkout.order.id', static::NOT_NULL ),
$this->expectedField( 'checkout.order.status', 'COMPLETED' ),
$this->expectedNode(
'checkout.order.metaData',
[
$this->expectedField( 'key', 'test_key' ),
$this->expectedField( 'value', 'test value' ),
]
),
$this->expectedNode(
'checkout.order.feeLines.nodes',
[
$this->expectedField( 'name', 'Surcharge' ),
$this->expectedField( 'databaseId', static::NOT_NULL ),
$this->expectedField( 'orderId', static::NOT_NULL ),
$this->expectedField( 'amount', static::NOT_NULL ),
$this->expectedField( 'taxStatus', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.taxLines.nodes',
[
$this->expectedField( 'label', 'VAT' ),
$this->expectedField( 'rateCode', static::NOT_NULL ),
$this->expectedField( 'taxTotal', static::NOT_NULL ),
$this->expectedField( 'shippingTaxTotal', static::NOT_NULL ),
$this->expectedField( 'isCompound', static::NOT_NULL ),
$this->expectedField( 'taxRate', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.lineItems.nodes',
[
$this->expectedField( 'productId', $product_ids[0] ),
$this->expectedField( 'quantity', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
$this->expectedField( 'subtotal', static::NOT_NULL ),
$this->expectedField( 'subtotalTax', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxStatus', static::NOT_NULL ),
$this->expectedField( 'product.node.id', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.lineItems.nodes',
[
$this->expectedField( 'productId', $product_ids[1] ),
$this->expectedField( 'quantity', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
$this->expectedField( 'subtotal', static::NOT_NULL ),
$this->expectedField( 'subtotalTax', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxStatus', static::NOT_NULL ),
$this->expectedField( 'product.node.id', static::NOT_NULL ),
]
),
$this->expectedField( 'checkout.customer.id', 'guest' ),
$this->expectedField( 'checkout.result', 'success' ),
$this->expectedField( 'checkout.redirect', static::NOT_NULL ),
];
$this->assertQuerySuccessful( $response, $expected );
// Confirm cart empty after successful checkout.
$query = $this->getCartQuery();
$response = $this->graphql( compact( 'query' ) );
$expected = [
$this->expectedField( 'cart.contents.nodes', [] ),
$this->expectedField( 'cart.total', '$0.00' ),
];
$this->assertQuerySuccessful( $response, $expected );
}
/**
* Returns a new Stripe Customer object
*
* @param string $email Customer email
*
* @return array
*/
private function create_stripe_customer( $email ) {
$customer = \Stripe\Customer::create( [ 'email' => $email ] );
// use --debug flag to view.
codecept_debug( $customer );
return $customer;
}
/**
* Creates a new Stripe Source object, attaches it to the provided customer,
* and returns the Source object
*
* @param array $customer Customer object
*
* @return array
*/
private function create_stripe_source( $customer ) {
$source = \Stripe\Customer::createSource(
$customer['id'],
[ 'source' => 'tok_visa' ]
);
// use --debug flag to view.
codecept_debug( $source );
return $source;
}
/**
* Creates a new Payment Intent object, assigns an amount and customer,
* and returns the Payment Intent object
*
* This payment intent is meant to be processed by WooCommerce upon validation
* so do not `confirm=true` must not be passed as a parameter.
*
* @param array $customer Customer object
*
* @return array
*/
private function create_stripe_payment_intent( $amount, $customer ) {
$payment_intent = \Stripe\PaymentIntent::create(
[
'amount' => $amount,
'currency' => 'gbp',
'payment_method_types' => [ 'card' ],
'customer' => $customer['id'],
'payment_method' => $customer['invoice_settings']['default_payment_method'],
]
);
// use --debug flag to view.
codecept_debug( $payment_intent );
return $payment_intent;
}
public function testCheckoutMutationWithStripe() {
$this->markTestSkipped( 'This test is for example purposes only and should NOT be run in CI.' );
WC()->customer->set_billing_email( 'superfreak500@gmail.com' );
WC()->customer->save();
// Add items to the cart.
$product_ids = [
$this->factory->product->createSimple(),
$this->factory->product->createSimple(),
];
WC()->cart->add_to_cart( $product_ids[0], 1 );
WC()->cart->add_to_cart( $product_ids[1], 1 );
$amount = (int) floatval( WC()->cart->get_cart_contents_total() + WC()->cart->get_cart_contents_tax() ) * 100;
try {
$stripe_customer = $this->create_stripe_customer( 'superfreak500@gmail.com' );
$stripe_source = $this->create_stripe_source( $stripe_customer );
$payment_intent = $this->create_stripe_payment_intent( $amount, $stripe_customer );
} catch ( \Stripe\Exception\AuthenticationException $e ) {
$this->markTestSkipped( $e->getMessage() );
}
$input = [
'paymentMethod' => 'stripe',
'metaData' => [
[
'key' => '_stripe_source_id',
'value' => $stripe_source['id'],
],
[
'key' => '_stripe_customer_id',
'value' => $stripe_customer['id'],
],
[
'key' => '_stripe_intent_id',
'value' => $payment_intent['id'],
],
],
];
$variables = [ 'input' => $this->getCheckoutInput( $input ) ];
// Remove "metaData" value field and "redirect" link from the mutation output.
$query = '
mutation checkout( $input: CheckoutInput! ) {
checkout( input: $input ) {
clientMutationId
order {
id
status
total
databaseId
metaData { key }
lineItems {
nodes {
productId
variationId
quantity
taxClass
subtotal
subtotalTax
total
totalTax
taxStatus
product {
node {
... on SimpleProduct {
id
}
... on VariableProduct {
id
}
}
}
variation {
node { id }
}
}
}
}
result
}
}
';
/**
* Assertion One
*
* Test mutation and input.
*/
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedField( 'checkout.order.id', static::NOT_NULL ),
$this->expectedField( 'checkout.order.status', 'PROCESSING' ),
$this->expectedNode(
'checkout.order.metaData',
[ $this->expectedField( 'key', '_stripe_source_id' ) ]
),
$this->expectedNode(
'checkout.order.metaData',
[ $this->expectedField( 'key', '_stripe_customer_id' ) ]
),
$this->expectedNode(
'checkout.order.metaData',
[ $this->expectedField( 'key', '_stripe_intent_id' ) ]
),
$this->expectedNode(
'checkout.order.lineItems.nodes',
[
$this->expectedField( 'productId', $product_ids[0] ),
$this->expectedField( 'quantity', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
$this->expectedField( 'subtotal', static::NOT_NULL ),
$this->expectedField( 'subtotalTax', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxStatus', static::NOT_NULL ),
$this->expectedField( 'product.node.id', static::NOT_NULL ),
]
),
$this->expectedNode(
'checkout.order.lineItems.nodes',
[
$this->expectedField( 'productId', $product_ids[1] ),
$this->expectedField( 'quantity', static::NOT_NULL ),
$this->expectedField( 'taxClass', static::NOT_NULL ),
$this->expectedField( 'subtotal', static::NOT_NULL ),
$this->expectedField( 'subtotalTax', static::NOT_NULL ),
$this->expectedField( 'total', static::NOT_NULL ),
$this->expectedField( 'totalTax', static::NOT_NULL ),
$this->expectedField( 'taxStatus', static::NOT_NULL ),
$this->expectedField( 'product.node.id', static::NOT_NULL ),
]
),
$this->expectedField( 'checkout.result', 'success' ),
];
$this->assertQuerySuccessful( $response, $expected );
// Confirm cart empty after successful checkout.
$query = $this->getCartQuery();
$response = $this->graphql( compact( 'query' ) );
$expected = [
$this->expectedField( 'cart.contents.nodes', [] ),
$this->expectedField( 'cart.total', '$0.00' ),
];
$this->assertQuerySuccessful( $response, $expected );
}
public function testCheckoutMutationCartItemValidation() {
add_filter( 'woocommerce_hold_stock_for_checkout', '__return_false' );
$product_id = $this->factory->product->createSimple(
[
'manage_stock' => true,
'stock_quantity' => 3,
]
);
$key = WC()->cart->add_to_cart( $product_id, 3 );
WC()->cart->set_quantity( $key, 5 );
$input = [
'account' => [
'username' => 'test_user_1',
'password' => 'test_pass',
],
];
$variables = [ 'input' => $this->getCheckoutInput( $input ) ];
$query = $this->getCheckoutMutation();
/**
* Assertion One
*
* Ensure that checkout failed when stock is too low.
*/
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [ $this->expectedField( 'checkout', static::IS_NULL ) ];
$this->assertQueryError( $response, $expected );
}
public function testCheckoutMutationWithFees() {
add_filter( 'woocommerce_hold_stock_for_checkout', '__return_false' );
$product_id = $this->factory->product->createSimple();
WC()->cart->add_to_cart( $product_id, 1 );
$input = [
'fees' => [
[
'name' => 'Extra Fee',
'amount' => 10,
'taxable' => true,
],
],
];
$variables = [ 'input' => $this->getCheckoutInput( $input ) ];
$query = $this->getCheckoutMutation();
/**
* Assertion One
*
* Ensure that checkout failed when stock is too low.
*/
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedField( 'checkout.order.id', static::NOT_NULL ),
$this->expectedField( 'checkout.order.feeLines.nodes.#.name', 'Extra Fee' ),
$this->expectedField( 'checkout.order.feeLines.nodes.#.amount', "10" ),
$this->expectedField( 'checkout.order.feeLines.nodes.#.total', "10" ),
];
$this->assertQuerySuccessful( $response, $expected );
}
/**
* Tests that metadata passed in the checkout input is available
* on the order object in the woocommerce_checkout_order_processed hook.
*
* @see https://github.com/wp-graphql/wp-graphql-woocommerce/issues/932
*/
public function testCheckoutMetaDataAvailableInProcessedHook() {
add_filter( 'woocommerce_hold_stock_for_checkout', '__return_false' );
$product_id = $this->factory->product->createSimple();
\WC()->cart->add_to_cart( $product_id, 1 );
// Listen for the hook and capture the order's meta.
$captured_meta = null;
add_action(
'woocommerce_checkout_order_processed',
function ( $order_id, $data, $order ) use ( &$captured_meta ) {
$captured_meta = $order->get_meta( 'checkout_source' );
},
10,
3
);
$input = $this->getCheckoutInput(
[
'metaData' => [
[
'key' => 'checkout_source',
'value' => 'graphql_test',
],
],
]
);
$variables = [ 'input' => $input ];
$query = $this->getCheckoutMutation();
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedField( 'checkout.order.id', static::NOT_NULL ),
];
$this->assertQuerySuccessful( $response, $expected );
// Verify the hook received the order with meta already saved.
$this->assertEquals( 'graphql_test', $captured_meta, 'woocommerce_checkout_order_processed hook should receive the order with updated meta data.' );
}
}