diff --git a/README.md b/README.md index e9cbf149..d42fd6ab 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Docs • AxisTaylor • Join Slack -[![Actions Status](https://github.com/wp-graphql/wp-graphql-woocommerce/workflows/WordPress%20Coding%20Standards/badge.svg)](https://github.com/wp-graphql/wp-graphql-woocommerce/actions?query=workflow%3A%22WordPress+Coding+Standards%22) [![Coverage Status](https://coveralls.io/repos/github/wp-graphql/wp-graphql-woocommerce/badge.svg?branch=develop)](https://coveralls.io/github/wp-graphql/wp-graphql-woocommerce?branch=develop) [![Financial Contributors on Open Collective](https://opencollective.com/woographql/all/badge.svg?label=financial+contributors)](https://opencollective.com/woographql) [![Actions Status](https://github.com/wp-graphql/wp-graphql-woocommerce/workflows/Testing%20Integration/badge.svg)](https://github.com/wp-graphql/wp-graphql-woocommerce/actions?query=workflow%3A%22Testing+Integration%22) +[![Actions Status](https://github.com/wp-graphql/wp-graphql-woocommerce/workflows/Testing%20Integration/badge.svg)](https://github.com/wp-graphql/wp-graphql-woocommerce/actions?query=workflow%3A%22Testing+Integration%22) [![Actions Status](https://github.com/wp-graphql/wp-graphql-woocommerce/workflows/WordPress%20Coding%20Standards/badge.svg)](https://github.com/wp-graphql/wp-graphql-woocommerce/actions?query=workflow%3A%22WordPress+Coding+Standards%22) [![Coverage Status](https://coveralls.io/repos/github/wp-graphql/wp-graphql-woocommerce/badge.svg?branch=develop)](https://coveralls.io/github/wp-graphql/wp-graphql-woocommerce?branch=develop) [![Financial Contributors on Open Collective](https://opencollective.com/woographql/all/badge.svg?label=financial+contributors)](https://opencollective.com/woographql) ## Quick Install 1. Install & activate [WooCommerce](https://woocommerce.com/) diff --git a/bin/install-test-env.sh b/bin/install-test-env.sh index 976ded0a..a12c0e5b 100644 --- a/bin/install-test-env.sh +++ b/bin/install-test-env.sh @@ -149,6 +149,8 @@ configure_wordpress() { setup_woocommerce() { echo "Installing & Activating WooCommerce" wp plugin install woocommerce --activate + echo "Installing & Activating WooCommerce-Gateway-Stripe" + wp plugin install woocommerce-gateway-stripe --activate } setup_wpgraphql() { diff --git a/includes/data/mutation/class-checkout-mutation.php b/includes/data/mutation/class-checkout-mutation.php index 00600401..599382ae 100644 --- a/includes/data/mutation/class-checkout-mutation.php +++ b/includes/data/mutation/class-checkout-mutation.php @@ -525,6 +525,11 @@ class Checkout_Mutation { throw new UserError( __( 'Unable to create order.', 'wp-graphql-woocommerce' ) ); } + // Add meta data. + if ( ! empty( $input['metaData'] ) ) { + self::add_order_meta( $order_id, $input['metaData'], $input, $context ); + } + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound do_action( 'woocommerce_checkout_order_processed', $order_id, $data, $order ); @@ -633,7 +638,7 @@ class Checkout_Mutation { if ( $meta_data ) { foreach ( $meta_data as $meta ) { - $order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + $order->add_meta_data( $meta['key'], $meta['value'] ); } } diff --git a/includes/mutation/class-checkout.php b/includes/mutation/class-checkout.php index 454b7dd4..9b5bec4f 100644 --- a/includes/mutation/class-checkout.php +++ b/includes/mutation/class-checkout.php @@ -146,11 +146,6 @@ class Checkout { throw new UserError( $order_id->get_error_message( 'checkout-error' ) ); } - // Add meta data. - if ( ! empty( $input['metaData'] ) ) { - Checkout_Mutation::add_order_meta( $order_id, $input['metaData'], $input, $context, $info ); - } - /** * Action called after checking out. * diff --git a/tests/wpunit/CheckoutMutationsTest.php b/tests/wpunit/CheckoutMutationTest.php similarity index 70% rename from tests/wpunit/CheckoutMutationsTest.php rename to tests/wpunit/CheckoutMutationTest.php index 2aaa4ca1..5f23df99 100644 --- a/tests/wpunit/CheckoutMutationsTest.php +++ b/tests/wpunit/CheckoutMutationTest.php @@ -2,7 +2,7 @@ use WPGraphQL\Type\WPEnumType; -class CheckoutMutationsTest extends \Codeception\TestCase\WPTestCase { +class CheckoutMutationTest extends \Codeception\TestCase\WPTestCase { public function setUp() { // before parent::setUp(); @@ -26,7 +26,7 @@ class CheckoutMutationsTest extends \Codeception\TestCase\WPTestCase { update_option( 'woocommerce_calc_taxes', 'yes' ); update_option( 'woocommerce_tax_round_at_subtotal', 'no' ); - // Enable payment gateway. + // Enable payment gateways. update_option( 'woocommerce_bacs_settings', array( @@ -38,6 +38,32 @@ class CheckoutMutationsTest extends \Codeception\TestCase\WPTestCase { ) ); + update_option( + 'woocommerce_stripe_settings', + array( + 'enabled' => 'yes', + 'title' => 'Credit Card (Stripe)', + 'description' => 'Pay with your credit card via Stripe', + 'webhook' => '', + 'testmode' => 'yes', + 'test_publishable_key' => STRIPE_API_PUBLISHABLE_KEY, + 'test_secret_key' => STRIPE_API_SECRET_KEY, + 'test_webhook_secret' => '', + 'publishable_key' => '', + 'secret_key' => '', + 'webhook_secret' => '', + 'inline_cc_form' => 'no', + 'statement_descriptor' => '', + 'capture' => 'yes', + 'payment_request' => 'yes', + 'payment_request_button_type' => 'buy', + 'payment_request_button_theme' => 'dark', + 'payment_request_button_height' => '44', + 'saved_cards' => 'yes', + 'logging' => 'no', + ) + ); + // Additional cart fees. add_action( 'woocommerce_cart_calculate_fees', @@ -76,176 +102,178 @@ class CheckoutMutationsTest extends \Codeception\TestCase\WPTestCase { parent::tearDown(); } - private function checkout( $input ) { - $mutation = ' - mutation checkout( $input: CheckoutInput! ) { - checkout( input: $input ) { - clientMutationId - order { - id - orderId - currency - orderVersion - date - modified - status - discountTotal - discountTax - shippingTotal - shippingTax - cartTax - total - totalTax - subtotal - orderNumber - orderKey - createdVia - pricesIncludeTax - parent { + private function checkout( $input, $mutation = null ) { + if ( ! $mutation ) { + $mutation = ' + mutation checkout( $input: CheckoutInput! ) { + checkout( input: $input ) { + clientMutationId + order { id + orderId + 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 + } + paymentMethod + paymentMethodTitle + transactionId + dateCompleted + datePaid + cartHash + shippingAddressMapUrl + hasBillingAddress + hasShippingAddress + isDownloadPermitted + needsShippingAddress + hasDownloadableItem + downloadableItems { + nodes { + url + accessExpires + downloadId + downloadsRemaining + name + product { + productId + } + download { + downloadId + } + } + } + needsPayment + needsProcessing + metaData { + key + value + } + couponLines { + nodes { + itemId + orderId + code + discount + discountTax + coupon { + id + } + } + } + feeLines { + nodes { + itemId + orderId + amount + name + taxStatus + total + totalTax + taxClass + } + } + shippingLines { + nodes { + itemId + orderId + methodTitle + total + totalTax + taxClass + } + } + taxLines { + nodes { + rateCode + label + taxTotal + shippingTaxTotal + isCompound + taxRate { + rateId + } + } + } + lineItems { + nodes { + productId + variationId + quantity + taxClass + subtotal + subtotalTax + total + totalTax + taxStatus + product { + ... on SimpleProduct { + id + } + ... on VariableProduct { + id + } + } + variation { + 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 - } - paymentMethod - paymentMethodTitle - transactionId - dateCompleted - datePaid - cartHash - shippingAddressMapUrl - hasBillingAddress - hasShippingAddress - isDownloadPermitted - needsShippingAddress - hasDownloadableItem - downloadableItems { - nodes { - url - accessExpires - downloadId - downloadsRemaining - name - product { - productId - } - download { - downloadId - } - } - } - needsPayment - needsProcessing - metaData { - key - value - } - couponLines { - nodes { - itemId - orderId - code - discount - discountTax - coupon { - id - } - } - } - feeLines { - nodes { - itemId - orderId - amount - name - taxStatus - total - totalTax - taxClass - } - } - shippingLines { - nodes { - itemId - orderId - methodTitle - total - totalTax - taxClass - } - } - taxLines { - nodes { - rateCode - label - taxTotal - shippingTaxTotal - isCompound - taxRate { - rateId - } - } - } - lineItems { - nodes { - productId - variationId - quantity - taxClass - subtotal - subtotalTax - total - totalTax - taxStatus - product { - ... on SimpleProduct { - id - } - ... on VariableProduct { - id - } - } - variation { - id - } - } - } + result + redirect } - customer { - id - } - result - redirect } - } - '; + '; + } $actual = graphql( array( @@ -258,8 +286,46 @@ class CheckoutMutationsTest extends \Codeception\TestCase\WPTestCase { return $actual; } + private function create_stripe_customer( $email ) { + $customer = \Stripe\Customer::create( array( 'email' => $email ) ); + + // use --debug flag to view. + codecept_debug( $customer ); + + return $customer; + } + + private function create_stripe_source( $customer ) { + $source = \Stripe\Customer::createSource( + $customer['id'], + array( 'source' => 'tok_visa') + ); + + // use --debug flag to view. + codecept_debug( $source ); + + return $source; + } + + private function create_stripe_payment_intent( $amount, $customer ) { + $payment_intent = \Stripe\PaymentIntent::create( + array( + '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; + } + // tests - public function testCheckoutOrderMutation() { + public function testCheckoutMutation() { wp_set_current_user( $this->simple_customer ); $variable = $this->variation->create( $this->product->create_variable() ); $product_ids = array( @@ -466,7 +532,7 @@ class CheckoutMutationsTest extends \Codeception\TestCase\WPTestCase { $this->assertEquals( $expected, $actual ); } - public function testCheckoutOrderMutationWithNewAccount() { + public function testCheckoutMutationWithNewAccount() { $variable = $this->variation->create( $this->product->create_variable() ); $product_ids = array( $this->product->create_simple(), @@ -657,7 +723,7 @@ class CheckoutMutationsTest extends \Codeception\TestCase\WPTestCase { $this->assertEquals( $expected, $actual ); } - public function testCheckoutOrderMutationWithNoAccount() { + public function testCheckoutMutationWithNoAccount() { update_option( 'woocommerce_enable_guest_checkout', 'yes' ); $variable = $this->variation->create( $this->product->create_variable() ); $product_ids = array( @@ -852,7 +918,7 @@ class CheckoutMutationsTest extends \Codeception\TestCase\WPTestCase { $this->assertEquals( $expected, $actual ); } - public function testCheckoutOrderMutationWithPrepaidOrder() { + public function testCheckoutMutationWithPrepaidOrder() { update_option( 'woocommerce_enable_guest_checkout', 'yes' ); $product_ids = array( $this->product->create_simple( @@ -1033,4 +1099,392 @@ class CheckoutMutationsTest extends \Codeception\TestCase\WPTestCase { $this->assertEquals( $expected, $actual ); } + + public function testCheckoutMutationWithStripe() { + wp_set_current_user( $this->simple_customer ); + $variable = $this->variation->create( $this->product->create_variable() ); + $product_ids = array( + $this->product->create_simple(), + $this->product->create_simple(), + $variable['product'], + ); + $coupon = new WC_Coupon( + $this->coupon->create( array( '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] ); + WC()->cart->apply_coupon( $coupon->get_code() ); + + $amount = (int) floatval( WC()->cart->get_cart_contents_total() + WC()->cart->get_cart_contents_tax() ) * 100; + $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 ); + + $input = array( + 'clientMutationId' => 'someId', + 'paymentMethod' => 'stripe', + 'shippingMethod' => 'flat rate', + 'billing' => array( + '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', + ), + 'shipping' => array( + 'firstName' => 'May', + 'lastName' => 'Parker', + 'address1' => '20 Ingram St', + 'city' => 'New York City', + 'state' => 'NY', + 'postcode' => '12345', + 'country' => 'US', + ), + 'metaData' => array( + array( + 'key' => '_stripe_source_id', + 'value' => $stripe_source['id'], + ), + array( + 'key' => '_stripe_customer_id', + 'value' => $stripe_customer['id'], + ), + array( + 'key' => '_stripe_intent_id', + 'value' => $payment_intent['id'], + ), + ) + ); + + // Remove "metaData" value field and "redirect" link from the mutation output. + $modified_mutation = ' + mutation checkout( $input: CheckoutInput! ) { + checkout( input: $input ) { + clientMutationId + order { + id + orderId + 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 + } + paymentMethod + paymentMethodTitle + transactionId + dateCompleted + datePaid + cartHash + shippingAddressMapUrl + hasBillingAddress + hasShippingAddress + isDownloadPermitted + needsShippingAddress + hasDownloadableItem + downloadableItems { + nodes { + url + accessExpires + downloadId + downloadsRemaining + name + product { + productId + } + download { + downloadId + } + } + } + needsPayment + needsProcessing + metaData { + key + } + couponLines { + nodes { + itemId + orderId + code + discount + discountTax + coupon { + id + } + } + } + feeLines { + nodes { + itemId + orderId + amount + name + taxStatus + total + totalTax + taxClass + } + } + shippingLines { + nodes { + itemId + orderId + methodTitle + total + totalTax + taxClass + } + } + taxLines { + nodes { + rateCode + label + taxTotal + shippingTaxTotal + isCompound + taxRate { + rateId + } + } + } + lineItems { + nodes { + productId + variationId + quantity + taxClass + subtotal + subtotalTax + total + totalTax + taxStatus + product { + ... on SimpleProduct { + id + } + ... on VariableProduct { + id + } + } + variation { + id + } + } + } + } + customer { + id + } + result + } + } + '; + + /** + * Assertion One + * + * Test mutation and input. + */ + $actual = $this->checkout( $input, $modified_mutation ); + + // use --debug flag to view. + codecept_debug( $actual ); + + $this->assertArrayHasKey('data', $actual ); + $this->assertArrayHasKey('checkout', $actual['data'] ); + $this->assertArrayHasKey('order', $actual['data']['checkout'] ); + $this->assertArrayHasKey('id', $actual['data']['checkout']['order'] ); + $order = \WC_Order_Factory::get_order( $actual['data']['checkout']['order']['orderId'] ); + + // Get Available payment gateways. + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + + $expected = array( + 'data' => array( + 'checkout' => array( + 'clientMutationId' => 'someId', + 'order' => array_merge( + $this->order->print_query( $order->get_id() ), + array( + 'metaData' => array( + array( 'key' => 'is_vat_exempt', ), + array( 'key' => '_stripe_source_id' ), + array( 'key' => '_stripe_customer_id' ), + array( 'key' => '_stripe_intent_id' ), + array( 'key' => '_stripe_charge_captured' ), + array( 'key' => '_stripe_fee' ), + array( 'key' => '_stripe_net' ), + array( 'key' => '_stripe_currency' ), + ), + 'couponLines' => array( + 'nodes' => array_reverse( + array_map( + function( $item ) { + return array( + 'itemId' => $item->get_id(), + 'orderId' => $item->get_order_id(), + 'code' => $item->get_code(), + 'discount' => ! empty( $item->get_discount() ) ? $item->get_discount() : null, + 'discountTax' => ! empty( $item->get_discount_tax() ) ? $item->get_discount_tax() : null, + 'coupon' => null, + ); + }, + $order->get_items( 'coupon' ) + ) + ), + ), + 'feeLines' => array( + 'nodes' => array_reverse( + array_map( + function( $item ) { + return array( + 'itemId' => $item->get_id(), + 'orderId' => $item->get_order_id(), + 'amount' => $item->get_amount(), + 'name' => $item->get_name(), + 'taxStatus' => strtoupper( $item->get_tax_status() ), + 'total' => $item->get_total(), + 'totalTax' => ! empty( $item->get_total_tax() ) ? $item->get_total_tax() : null, + 'taxClass' => ! empty( $item->get_tax_class() ) + ? WPEnumType::get_safe_name( $item->get_tax_class() ) + : 'STANDARD', + ); + }, + $order->get_items( 'fee' ) + ) + ), + ), + 'shippingLines' => array( + 'nodes' => array_reverse( + array_map( + function( $item ) { + + return array( + 'itemId' => $item->get_id(), + 'orderId' => $item->get_order_id(), + 'methodTitle' => $item->get_method_title(), + 'total' => $item->get_total(), + 'totalTax' => !empty( $item->get_total_tax() ) + ? $item->get_total_tax() + : null, + 'taxClass' => ! empty( $item->get_tax_class() ) + ? $item->get_tax_class() === 'inherit' + ? WPEnumType::get_safe_name( 'inherit cart' ) + : WPEnumType::get_safe_name( $item->get_tax_class() ) + : 'STANDARD' + ); + }, + $order->get_items( 'shipping' ) + ) + ), + ), + 'taxLines' => array( + 'nodes' => array_reverse( + array_map( + function( $item ) { + return array( + 'rateCode' => $item->get_rate_code(), + 'label' => $item->get_label(), + 'taxTotal' => $item->get_tax_total(), + 'shippingTaxTotal' => $item->get_shipping_tax_total(), + 'isCompound' => $item->is_compound(), + 'taxRate' => array( 'rateId' => $item->get_rate_id() ), + ); + }, + $order->get_items( 'tax' ) + ) + ), + ), + 'lineItems' => array( + 'nodes' => array_values( + array_map( + function( $item ) { + return array( + 'productId' => $item->get_product_id(), + 'variationId' => ! empty( $item->get_variation_id() ) + ? $item->get_variation_id() + : null, + 'quantity' => $item->get_quantity(), + 'taxClass' => ! empty( $item->get_tax_class() ) + ? strtoupper( $item->get_tax_class() ) + : 'STANDARD', + 'subtotal' => ! empty( $item->get_subtotal() ) ? $item->get_subtotal() : null, + 'subtotalTax' => ! empty( $item->get_subtotal_tax() ) ? $item->get_subtotal_tax() : null, + 'total' => ! empty( $item->get_total() ) ? $item->get_total() : null, + 'totalTax' => ! empty( $item->get_total_tax() ) ? $item->get_total_tax() : null, + 'taxStatus' => strtoupper( $item->get_tax_status() ), + 'product' => array( 'id' => $this->product->to_relay_id( $item->get_product_id() ) ), + 'variation' => ! empty( $item->get_variation_id() ) + ? array( + 'id' => $this->variation->to_relay_id( $item->get_variation_id() ) + ) + : null, + ); + }, + $order->get_items() + ) + ), + ), + ) + ), + 'customer' => array( + 'id' => $this->customer->to_relay_id( $order->get_customer_id() ) + ), + 'result' => 'success', + ), + ) + ); + + $this->assertEquals( $expected, $actual ); + } } \ No newline at end of file