From 73bf91e1730a4e87ef7d8abaeffd00b1a666421a Mon Sep 17 00:00:00 2001 From: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:43:33 +0300 Subject: [PATCH 01/13] Pin Github actions to full-commit SHA --- .github/workflows/integration.yml | 2 +- .github/workflows/package-new.yml | 2 +- .github/workflows/package.yml | 2 +- .github/workflows/php.yml | 4 ++-- .github/workflows/spell-check.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index e02629141..a6128e58c 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -12,7 +12,7 @@ jobs: name: PHP ${{ matrix.php-versions }} WC ${{ matrix.wc-versions }} steps: - - uses: ddev/github-action-setup-ddev@v1 + - uses: ddev/github-action-setup-ddev@1c7ef18595da42355373cb6d9417a6f44d758b93 # v1 with: autostart: false diff --git a/.github/workflows/package-new.yml b/.github/workflows/package-new.yml index 2b12eca03..01303699e 100644 --- a/.github/workflows/package-new.yml +++ b/.github/workflows/package-new.yml @@ -27,7 +27,7 @@ jobs: create_archive: needs: check_version - uses: inpsyde/reusable-workflows/.github/workflows/build-plugin-archive.yml@main + uses: inpsyde/reusable-workflows/.github/workflows/build-plugin-archive.yml@a9af34f34e95cbe18703198c7e972e97ebcd7473 with: PHP_VERSION: 7.4 NODE_VERSION: 22 diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index c3e07d1bb..aa66cb1fa 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@v2 - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1 with: php-version: 7.4 diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 00ff24fcb..46c71ee47 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v2 - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1 with: php-version: ${{ matrix.php-versions }} @@ -22,7 +22,7 @@ jobs: run: composer validate - name: Install dependencies - uses: ramsey/composer-install@v1 + uses: ramsey/composer-install@994bb194a4fefcf39449ccf0f7766a4318f1ac76 # v1 with: composer-options: "--prefer-dist" diff --git a/.github/workflows/spell-check.yml b/.github/workflows/spell-check.yml index 14c28abac..78ea83c0d 100644 --- a/.github/workflows/spell-check.yml +++ b/.github/workflows/spell-check.yml @@ -14,7 +14,7 @@ jobs: - name: Check spelling id: spelling - uses: crate-ci/typos@v1.30.2 + uses: crate-ci/typos@7bc041cbb7ca9167c9e0e4ccbb26f48eb0f9d4e0 # v1.30.2 with: # Path to config file config: .github/workflows-config/typos.toml From 0284e38e0cd2bd002f23a7d3ad22bb216c3ebc1c Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Mon, 23 Jun 2025 15:03:31 +0200 Subject: [PATCH 02/13] Add test factories and traits for integration tests - Add ProductFactory, OrderFactory, and CouponFactory - Add CreateTestOrders and CreateTestProducts traits for reusable test setup - Add ProductPresets and DiscountPresets fixtures - Implement existence checks to prevent duplicate product/coupon creation --- .../PHPUnit/Factories/CouponFactory.php | 92 +++++++ .../PHPUnit/Factories/OrderFactory.php | 227 ++++++++++++++++++ .../PHPUnit/Factories/ProductFactory.php | 168 +++++++++++++ .../PHPUnit/Fixtures/DiscountPresets.php | 26 ++ .../PHPUnit/Fixtures/ProductPresets.php | 49 ++++ .../PHPUnit/IntegrationMockedTestCase.php | 120 +++------ .../PHPUnit/Traits/CleansTestData.php | 25 ++ .../PHPUnit/Traits/CreateTestOrders.php | 49 ++++ .../PHPUnit/Traits/CreateTestProducts.php | 60 +++++ .../PHPUnit/VaultingSubscriptionsTest.php | 9 +- 10 files changed, 727 insertions(+), 98 deletions(-) create mode 100644 tests/integration/PHPUnit/Factories/CouponFactory.php create mode 100644 tests/integration/PHPUnit/Factories/OrderFactory.php create mode 100644 tests/integration/PHPUnit/Factories/ProductFactory.php create mode 100644 tests/integration/PHPUnit/Fixtures/DiscountPresets.php create mode 100644 tests/integration/PHPUnit/Fixtures/ProductPresets.php create mode 100644 tests/integration/PHPUnit/Traits/CleansTestData.php create mode 100644 tests/integration/PHPUnit/Traits/CreateTestOrders.php create mode 100644 tests/integration/PHPUnit/Traits/CreateTestProducts.php diff --git a/tests/integration/PHPUnit/Factories/CouponFactory.php b/tests/integration/PHPUnit/Factories/CouponFactory.php new file mode 100644 index 000000000..6091368ad --- /dev/null +++ b/tests/integration/PHPUnit/Factories/CouponFactory.php @@ -0,0 +1,92 @@ +createCoupon($preset); + } + + /** + * @param array $preset + * @return WC_Coupon + */ + private function createCoupon(array $preset): WC_Coupon + { + $coupon = new WC_Coupon(); + $coupon->set_code($preset['coupon_code']); + $coupon->set_discount_type($preset['type']); + $coupon->set_amount($preset['amount']); + $coupon->set_status('publish'); + $coupon->save(); + + $this->created_coupon_ids[] = $coupon->get_id(); + + return $coupon; + } + + /** + * @param string $coupon_code + * @return bool + */ + public function exists(string $coupon_code): bool + { + return (bool) wc_get_coupon_id_by_code($coupon_code); + } + + /** + * @param string $coupon_code + * @return WC_Coupon|null + */ + public function getByCode(string $coupon_code): ?WC_Coupon + { + $coupon_id = wc_get_coupon_id_by_code($coupon_code); + + return $coupon_id ? new WC_Coupon($coupon_id) : null; + } + + /** + * Delete all created coupons + */ + public function cleanup(): void + { + foreach ($this->created_coupon_ids as $coupon_id) { + wp_delete_post($coupon_id, true); + } + + $this->created_coupon_ids = []; + } + + /** + * @return array + */ + public function getCreatedIds(): array + { + return $this->created_coupon_ids; + } +} diff --git a/tests/integration/PHPUnit/Factories/OrderFactory.php b/tests/integration/PHPUnit/Factories/OrderFactory.php new file mode 100644 index 000000000..8e9c6c5f2 --- /dev/null +++ b/tests/integration/PHPUnit/Factories/OrderFactory.php @@ -0,0 +1,227 @@ +product_factory = $product_factory ?? new ProductFactory(); + $this->coupon_factory = $coupon_factory ?? new CouponFactory(); + } + + /** + * @param int $customer_id + * @param string $payment_method + * @param array $product_presets + * @param array $discount_presets + * @param bool $set_paid + * @return WC_Order + * @throws \WC_Data_Exception + */ + public function create( + int $customer_id, + string $payment_method, + array $product_presets, + array $discount_presets = [], + bool $set_paid = true + ): WC_Order { + $products = $this->resolveProductPresets($product_presets); + $discounts = $this->resolveDiscountPresets($discount_presets); + + $order = wc_create_order([ + 'customer_id' => $customer_id, + 'set_paid' => $set_paid, + ]); + + if (is_wp_error($order)) { + throw new \WC_Data_Exception('order_creation_failed', 'Failed to create order'); + } + + $this->setBillingAddress($order); + $this->addProductsToOrder($order, $products); + $this->applyDiscountsToOrder($order, $discounts); + + $order->set_payment_method($payment_method); + $order->calculate_totals(); + $order->save(); + + return $order; + } + + /** + * @param WC_Order $order + */ + private function setBillingAddress(WC_Order $order): void + { + $order->set_billing_first_name('John'); + $order->set_billing_last_name('Doe'); + $order->set_billing_address_1('969 Market'); + $order->set_billing_city('San Francisco'); + $order->set_billing_state('CA'); + $order->set_billing_postcode('94103'); + $order->set_billing_country('US'); + $order->set_billing_email('john.doe@example.com'); + $order->set_billing_phone('(555) 555-5555'); + } + + /** + * @param WC_Order $order + * @param array $products + * @throws \WC_Data_Exception + */ + private function addProductsToOrder(WC_Order $order, array $products): void + { + foreach ($products as $product_data) { + $product_sku = $product_data['sku']; + $product_id = wc_get_product_id_by_sku($product_sku); + $variation_id = $product_data['variation_id'] ?? 0; + $product_type = $product_data['type'] ?? 'simple'; + + $product = wc_get_product($variation_id ?: $product_id); + + if (!$product) { + throw new \WC_Data_Exception('invalid_product', "Product {$product_id} not found"); + } + + // Use appropriate item class based on product type + $item = $this->createOrderItem($product_type, $product_data, $product); + + if ($variation_id && $product->is_type('variation')) { + $item->set_variation_data($product->get_variation_attributes()); + } + + $order->add_item($item); + } + } + + /** + * @param WC_Order $order + * @param array $discounts + */ + private function applyDiscountsToOrder(WC_Order $order, array $discounts): void + { + foreach ($discounts as $discount) { + if (isset($discount['coupon_code'])) { + $order->apply_coupon($discount['coupon_code']); + } + + if (isset($discount['fee'])) { + $fee = new WC_Order_Item_Fee(); + $fee->set_props([ + 'name' => $discount['fee']['name'], + 'amount' => -abs($discount['fee']['amount']), + 'total' => -abs($discount['fee']['amount']), + ]); + $order->add_item($fee); + } + } + } + + /** + * @param array $product_presets + * @return array + * @throws \WC_Data_Exception + */ + private function resolveProductPresets(array $product_presets): array + { + $available_presets = ProductPresets::get(); + $products = []; + + foreach ($product_presets as $preset) { + if (is_string($preset)) { + if (!isset($available_presets[$preset])) { + throw new \WC_Data_Exception('invalid_preset', "Product preset '{$preset}' not found"); + } + $products[] = $available_presets[$preset]; + } elseif (is_array($preset)) { + $preset_name = $preset['preset']; + $quantity = $preset['quantity'] ?? 1; + + if (!isset($available_presets[$preset_name])) { + throw new \WC_Data_Exception('invalid_preset', "Product preset '{$preset_name}' not found"); + } + + $product_data = $available_presets[$preset_name]; + $product_data['quantity'] = $quantity; + $products[] = $product_data; + } + } + + return $products; + } + + /** + * @param array $discount_presets + * @return array + * @throws \WC_Data_Exception + */ + private function resolveDiscountPresets(array $discount_presets): array + { + $available_presets = DiscountPresets::get(); + $discounts = []; + + foreach ($discount_presets as $preset) { + if (!isset($available_presets[$preset])) { + throw new \WC_Data_Exception('invalid_preset', "Discount preset '{$preset}' not found"); + } + $discounts[] = $available_presets[$preset]; + } + + return $discounts; + } + + /** + * Delete all created orders + */ + public function cleanup(): void + { + foreach ($this->created_order_ids as $order_id) { + wp_delete_post($order_id, true); + } + + $this->created_order_ids = []; + } + + /** + * @return array + */ + public function getCreatedIds(): array + { + return $this->created_order_ids; + } + + /** + * @param string $product_type + * @param array $product_data + * @param \WC_Product $product + * @return \WC_Order_Item_Product + */ + private function createOrderItem(string $product_type, array $product_data, \WC_Product $product): \WC_Order_Item_Product + { + $item = new \WC_Order_Item_Product(); + + $item->set_props([ + 'product_id' => $product->get_id(), + 'variation_id' => $product_data['variation_id'] ?? 0, + 'quantity' => $product_data['quantity'], + 'subtotal' => $product->get_price() * $product_data['quantity'], + 'total' => $product->get_price() * $product_data['quantity'], + ]); + + return $item; + } +} diff --git a/tests/integration/PHPUnit/Factories/ProductFactory.php b/tests/integration/PHPUnit/Factories/ProductFactory.php new file mode 100644 index 000000000..50f74e1ba --- /dev/null +++ b/tests/integration/PHPUnit/Factories/ProductFactory.php @@ -0,0 +1,168 @@ +createVariableProduct($preset); + case 'subscription': + return $this->createSubscriptionProduct($preset); + case 'simple': + default: + return $this->createSimpleProduct($preset); + } + } + + /** + * @param array $preset + * @return WC_Product_Simple + */ + private function createSimpleProduct(array $preset): WC_Product_Simple + { + $product = new WC_Product_Simple(); + $product_id = wc_get_product_id_by_sku($preset['sku']); + $product->set_sku($preset['sku']); + $product->set_id($product_id); + $product->set_name($preset['name']); + $product->set_regular_price($preset['price']); + $product->set_status('publish'); + $product->save(); + + $this->created_product_ids[] = $product_id; + + return $product; + } + + /** + * @param array $preset + * @return WC_Product_Variation + */ + private function createVariableProduct(array $preset): WC_Product_Variation + { + // Create parent variable product + $parent = new WC_Product_Variable(); + $product_id = wc_get_product_id_by_sku($preset['sku']); + $parent->set_sku($preset['sku']); + $parent->set_id($product_id); + $parent->set_name($preset['name']); + $parent->set_status('publish'); + $parent->save(); + + // Create variation + $variation = new WC_Product_Variation(); + $variation->set_id($preset['variation_id']); + $variation->set_parent_id($preset['product_id']); + $variation->set_regular_price($preset['price']); + $variation->set_attributes(['color' => 'red']); + $variation->set_status('publish'); + $variation->save(); + + $this->created_product_ids[] = $product_id; + $this->created_product_ids[] = $preset['variation_id']; + + return $variation; + } + + /** + * @param array $preset + * @return \WC_Product_Subscription + */ + private function createSubscriptionProduct(array $preset): \WC_Product_Subscription + { + $product = new \WC_Product_Subscription(); + $product_id = wc_get_product_id_by_sku($preset['sku']); + $product->set_id($product_id); + $product->set_name($preset['name']); + $product->set_regular_price($preset['price']); + $product->set_price($preset['price']); + $product->set_sku($preset['sku']); + $product->set_manage_stock(false); + $product->set_tax_status('taxable'); + $product->set_downloadable(false); + $product->set_virtual(false); + $product->set_stock_status('instock'); + $product->set_weight('1.1'); + + // Subscription-specific properties + $product->set_subscription_period($preset['subscription_period']); + $product->set_subscription_period_interval($preset['subscription_period_interval']); + $product->set_subscription_length($preset['subscription_length']); + $product->set_subscription_trial_period($preset['subscription_trial_period']); + $product->set_subscription_trial_length($preset['subscription_trial_length']); + $product->set_subscription_price($preset['subscription_price']); + $product->set_subscription_sign_up_fee($preset['subscription_sign_up_fee']); + + $product->set_status('publish'); + $product->save(); + + $this->created_product_ids[] = $product_id; + + return $product; + } + + /** + * @param string $sku + * @return bool + */ + public function exists(string $sku): bool + { + $existing_product_id = wc_get_product_id_by_sku($sku); + return (bool) $existing_product_id; + } + + /** + * Delete all created products + */ + public function cleanup(): void + { + foreach ($this->created_product_ids as $product_id) { + wp_delete_post($product_id, true); + } + + $this->created_product_ids = []; + } +} diff --git a/tests/integration/PHPUnit/Fixtures/DiscountPresets.php b/tests/integration/PHPUnit/Fixtures/DiscountPresets.php new file mode 100644 index 000000000..019bf1411 --- /dev/null +++ b/tests/integration/PHPUnit/Fixtures/DiscountPresets.php @@ -0,0 +1,26 @@ + [ + 'coupon_code' => 'TEST10PERCENT', + 'type' => 'percent', + 'amount' => '10' + ], + 'fixed_5' => [ + 'coupon_code' => 'TEST5FIXED', + 'type' => 'fixed_cart', + 'amount' => '5.00' + ], + 'manual_discount' => [ + 'fee' => ['name' => 'Test Discount', 'amount' => 3.50] + ], + ]; + } +} diff --git a/tests/integration/PHPUnit/Fixtures/ProductPresets.php b/tests/integration/PHPUnit/Fixtures/ProductPresets.php new file mode 100644 index 000000000..581b37b2a --- /dev/null +++ b/tests/integration/PHPUnit/Fixtures/ProductPresets.php @@ -0,0 +1,49 @@ + [ + 'sku' => 'DUMMY_SIMPLE_SKU_01', + 'name' => 'Test Simple Product', + 'price' => '10.00', + 'quantity' => 1, + 'type' => 'simple' + ], + 'simple_expensive' => [ + 'sku' => 'DUMMY_SIMPLE_SKU_02', + 'name' => 'Test Expensive Product', + 'price' => '199.99', + 'quantity' => 1, + 'type' => 'simple' + ], + /*'variable' => [ + 'sku' => 'DUMMY_VARIABLE_SKU_01', + 'variation_id' => 20002, + 'name' => 'Test Variable Product', + 'price' => '25.00', + 'quantity' => 1, + 'type' => 'variable' + ],*/ + 'subscription' => [ + 'name' => 'Dummy Subscription Product', + 'price' => '10.00', + 'quantity' => 1, + 'type' => 'subscription', + 'sku' => 'DUMMY SUB SKU', + 'subscription_period' => 'day', + 'subscription_period_interval' => 1, + 'subscription_length' => 0, + 'subscription_trial_period' => '', + 'subscription_trial_length' => 0, + 'subscription_price' => 10, + 'subscription_sign_up_fee' => 0, + ] + ]; + } +} diff --git a/tests/integration/PHPUnit/IntegrationMockedTestCase.php b/tests/integration/PHPUnit/IntegrationMockedTestCase.php index f23360130..0d4028f51 100644 --- a/tests/integration/PHPUnit/IntegrationMockedTestCase.php +++ b/tests/integration/PHPUnit/IntegrationMockedTestCase.php @@ -18,6 +18,8 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit; use WooCommerce\PayPalCommerce\Helper\RedirectorStub; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use WooCommerce\PayPalCommerce\PPCP; +use WooCommerce\PayPalCommerce\Tests\Integration\Traits\CreateTestOrders; +use WooCommerce\PayPalCommerce\Tests\Integration\Traits\CreateTestProducts; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule; @@ -25,98 +27,25 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; class IntegrationMockedTestCase extends TestCase { - use MockeryPHPUnitIntegration; + use MockeryPHPUnitIntegration, CreateTestOrders, CreateTestProducts; public function setUp(): void { parent::setUp(); - $this->default_product_id = $this->createAProductIfNotProvided(); + //$this->default_product_id = $this->createAProductIfNotProvided(); + $this->customer_id = $this->createCustomerIfNotExists(); + + + $this->createTestProducts(); + //$this->createTestCoupons(); } - /** - * @param int $customer_id - * @param string $payment_method - * @param int $product_id - * @param bool $set_paid - * @return \WC_Order|\WP_Error - * @throws \WC_Data_Exception - */ - public function getMockedOrder(int $customer_id, string $payment_method, int $product_id, bool $set_paid = true) - { - $order = wc_create_order([ - 'customer_id' => $customer_id, - 'set_paid' => $set_paid, - 'billing' => [ - 'first_name' => 'John', - 'last_name' => 'Doe', - 'address_1' => '969 Market', - 'address_2' => '', - 'city' => 'San Francisco', - 'state' => 'CA', - 'postcode' => '94103', - 'country' => 'US', - 'email' => 'john.doe@example.com', - 'phone' => '(555) 555-5555' - ], - 'line_items' => [ - [ - 'product_id' => $product_id, - 'quantity' => 1 - ] - ], - ]); - $order->set_payment_method($payment_method); - // Make sure the order is properly saved - $order->save(); - - // Add the product to the order - $item = new WC_Order_Item_Product(); - $item->set_props([ - 'product_id' => $product_id, - 'quantity' => 1, - 'subtotal' => 10, - 'total' => 10, - ]); - $order->add_item($item); - $order->calculate_totals(); - $order->save(); - return $order; - } - - /** - * @param string $sku - * @return int - */ - public function createAProductIfNotProvided(string $sku = 'DUMMY SUB SKU'): int - { - $product_id = wc_get_product_id_by_sku($sku); - if (!$product_id) { - $product = new \WC_Product_Subscription(); - $product->set_props([ - 'name' => 'Dummy Subscription Product', - 'regular_price' => 10, - 'price' => 10, - 'sku' => 'DUMMY SUB SKU', - 'manage_stock' => false, - 'tax_status' => 'taxable', - 'downloadable' => false, - 'virtual' => false, - 'stock_status' => 'instock', - 'weight' => '1.1', - // Subscription-specific properties - 'subscription_period' => 'month', - 'subscription_period_interval' => 1, - 'subscription_length' => 0, // 0 means unlimited - 'subscription_trial_period' => '', - 'subscription_trial_length' => 0, - 'subscription_price' => 10, - 'subscription_sign_up_fee' => 0, - ]); - $product->save(); - $product_id = $product->get_id(); - } - return $product_id; - } + public function tearDown(): void + { + // This cleans up everything created during tests + //$this->cleanupTestData(); + parent::tearDown(); + } /** * @param array $overriddenServices @@ -219,11 +148,13 @@ class IntegrationMockedTestCase extends TestCase */ public function createSubscription(int $customer_id = 1, string $payment_method = 'ppcp-gateway', $sku = 'DUMMY SUB SKU'): WC_Subscription { - // Create a product if not provided - $product_id = $this->createAProductIfNotProvided($sku); - - $order = $this->getMockedOrder($customer_id, $payment_method, $product_id, $set_paid = true); + $product_id = wc_get_product_id_by_sku($sku); + $order = $this->getConfiguredOrder( + $this->customer_id, + $payment_method, + ['subscription'] + ); $subscription = new WC_Subscription(); $subscription->set_customer_id($customer_id); $subscription->set_payment_method($payment_method); @@ -259,7 +190,14 @@ class IntegrationMockedTestCase extends TestCase */ protected function createRenewalOrder(int $customer_id, string $gateway_id, int $subscription_id): WC_Order { - $renewal_order = $this->getMockedOrder($customer_id, $gateway_id, $this->default_product_id, false); + $renewal_order = $this->getConfiguredOrder( + $customer_id, + $gateway_id, + ['subscription'], + [], + false + ); + $renewal_order->update_meta_data('_subscription_renewal', $subscription_id); $renewal_order->update_meta_data('_subscription_renewal', $subscription_id); $renewal_order->save(); diff --git a/tests/integration/PHPUnit/Traits/CleansTestData.php b/tests/integration/PHPUnit/Traits/CleansTestData.php new file mode 100644 index 000000000..a07c972d1 --- /dev/null +++ b/tests/integration/PHPUnit/Traits/CleansTestData.php @@ -0,0 +1,25 @@ +order_factory)) { + $this->order_factory->cleanup(); + } + + if (isset($this->product_factory)) { + $this->product_factory->cleanup(); + } + + if (isset($this->coupon_factory)) { + $this->coupon_factory->cleanup(); + } + } +} diff --git a/tests/integration/PHPUnit/Traits/CreateTestOrders.php b/tests/integration/PHPUnit/Traits/CreateTestOrders.php new file mode 100644 index 000000000..029465ef2 --- /dev/null +++ b/tests/integration/PHPUnit/Traits/CreateTestOrders.php @@ -0,0 +1,49 @@ +product_factory)) { + $this->initializeFactories(); + } + + $this->order_factory = new OrderFactory($this->product_factory, $this->coupon_factory); + } + + /** + * Create a configured order using presets + */ + protected function getConfiguredOrder( + int $customer_id, + string $payment_method, + array $product_presets, + array $discount_presets = [], + bool $set_paid = true + ): WC_Order { + if (!isset($this->order_factory)) { + $this->initializeOrderFactory(); + } + + return $this->order_factory->create( + $customer_id, + $payment_method, + $product_presets, + $discount_presets, + $set_paid + ); + } +} diff --git a/tests/integration/PHPUnit/Traits/CreateTestProducts.php b/tests/integration/PHPUnit/Traits/CreateTestProducts.php new file mode 100644 index 000000000..0584e4d4d --- /dev/null +++ b/tests/integration/PHPUnit/Traits/CreateTestProducts.php @@ -0,0 +1,60 @@ +product_factory = new ProductFactory(); + $this->coupon_factory = new CouponFactory(); + } + + /** + * Create all test products from presets + * @throws \WC_Data_Exception + */ + protected function createTestProducts(): void + { + if (!isset($this->product_factory)) { + $this->initializeFactories(); + } + + foreach (array_keys(ProductPresets::get()) as $preset_name) { + $preset = ProductPresets::get()[$preset_name]; + // Only create if doesn't exist + if (!$this->product_factory->exists($preset['sku'])) { + $this->product_factory->createFromPreset($preset_name); + } + } + } + + /** + * Create all test coupons from presets + */ + protected function createTestCoupons(): void + { + if (!isset($this->coupon_factory)) { + $this->initializeFactories(); + } + + foreach (DiscountPresets::get() as $preset_name => $preset) { + // Only create coupons (skip manual fees) + if (isset($preset['coupon_code']) && !$this->coupon_factory->exists($preset['coupon_code'])) { + $this->coupon_factory->createFromPreset($preset_name); + } + } + } +} diff --git a/tests/integration/PHPUnit/VaultingSubscriptionsTest.php b/tests/integration/PHPUnit/VaultingSubscriptionsTest.php index e43db4fbf..7958041a7 100644 --- a/tests/integration/PHPUnit/VaultingSubscriptionsTest.php +++ b/tests/integration/PHPUnit/VaultingSubscriptionsTest.php @@ -25,13 +25,8 @@ class VaultingSubscriptionsTest extends IntegrationMockedTestCase { parent::setUp(); - // Common mock setup $this->mockPaymentTokensEndpoint = \Mockery::mock(PaymentTokensEndpoint::class); - - // Create customer and default product that can be reused - $this->customer_id = $this->createCustomerIfNotExists(); - $this->default_product_id = $this->createAProductIfNotProvided(); - } + } /** * Sets up a test container with common mocks @@ -172,7 +167,7 @@ class VaultingSubscriptionsTest extends IntegrationMockedTestCase { return [ 'PayPal Gateway' => [PayPalGateway::ID], - 'Credit Card Gateway' => [CreditCardGateway::ID] + //'Credit Card Gateway' => [CreditCardGateway::ID] ]; } From 3e37395073963dd3fa3d591464c65a9c4e7e78a3 Mon Sep 17 00:00:00 2001 From: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com> Date: Tue, 24 Jun 2025 15:51:15 +0300 Subject: [PATCH 03/13] Update .github/workflows/integration.yml Co-authored-by: Diego Curbelo --- .github/workflows/integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index a6128e58c..252f363ad 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -12,7 +12,7 @@ jobs: name: PHP ${{ matrix.php-versions }} WC ${{ matrix.wc-versions }} steps: - - uses: ddev/github-action-setup-ddev@1c7ef18595da42355373cb6d9417a6f44d758b93 # v1 + - uses: ddev/github-action-setup-ddev@1c7ef18595da42355373cb6d9417a6f44d758b93 # v1.10.1 with: autostart: false From 7b61115e39cea0918dbbead733fe5ecb7b165011 Mon Sep 17 00:00:00 2001 From: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com> Date: Tue, 24 Jun 2025 15:51:31 +0300 Subject: [PATCH 04/13] Update .github/workflows/php.yml Co-authored-by: Diego Curbelo --- .github/workflows/php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 46c71ee47..1eaa00b41 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -22,7 +22,7 @@ jobs: run: composer validate - name: Install dependencies - uses: ramsey/composer-install@994bb194a4fefcf39449ccf0f7766a4318f1ac76 # v1 + uses: ramsey/composer-install@a7320a0581dcd0432930c48a0e7ced67e6ec17e8 # v1.3.0 with: composer-options: "--prefer-dist" From 2265ebab7c6202f08487a2e6f7035fa5cd36e2a1 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 25 Jun 2025 12:03:24 +0200 Subject: [PATCH 05/13] Set contact module enabled by default --- modules/ppcp-wc-gateway/services.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index e6a5c05da..b1101dec7 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -2145,7 +2145,7 @@ return array( $feature_enabled = (bool) apply_filters( // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores -- feature flags use this convention 'woocommerce.feature-flags.woocommerce_paypal_payments.contact_module_enabled', - getenv( 'PCP_CONTACT_MODULE_ENABLED' ) === '1' + getenv( 'PCP_CONTACT_MODULE_ENABLED' ) !== '0' ); /** From f1afd3a76a70860aba031b2a6ac87f4cf3f242a4 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 25 Jun 2025 12:19:14 +0200 Subject: [PATCH 06/13] 3.0.7-rc1 release --- changelog.txt | 16 ++++++++++++++++ package.json | 2 +- readme.txt | 18 +++++++++++++++++- woocommerce-paypal-payments.php | 6 +++--- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index 77916063c..b041f4d38 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,21 @@ *** Changelog *** += 3.0.7 - xxxx-xx-xx = +* Enhancement - Deprecate `application_context` in favor of `experience_context` object #3431 + **NOTE**: If you were extending/modifying the `application_context` object programmatically, you may need to update your code to utilize `experience_context` for your customizations. +* Enhancement - Add Contact Module feature +* Enhancement - Add WooCommerce Tracks integration +* Enhancement - Onboarding notification for Firefox browser #3433 +* Enhancement - Reset BN code on plugin uninstall #3471 +* Enhancement - Add "Stay updated with PayPal" option in the old and new settings UI #3430 +* Enhancement - Add French Territories to the supported ACDC countries list #3438 +* Enhancement - Auto-enable logging during onboarding #3369 +* Fix - DUPLICATE_INVOICE_ID in Sandbox due to missing invoice prefix #3435 +* Fix - Subscription product could not be unlinked from PayPal Subscription #3429 +* Fix - PayPal button greyed out on single product page for variable products with >2 attributes #3395 +* Fix - APMs automatically enabled despite selecting "No, ..." during onboarding #3362 +* Fix - Ditch items logic does not work when using saved card payment #3476 + = 3.0.6 - 2025-05-27 = * Enhancement - Implement 3D secure check for Google Pay #3163 * Enhancement - Add options for "Disable Credit Cards" and "Language" #3226 diff --git a/package.json b/package.json index bf73a6a18..2d4ef6f40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "woocommerce-paypal-payments", - "version": "3.0.6", + "version": "3.0.7", "description": "WooCommerce PayPal Payments", "repository": "https://github.com/woocommerce/woocommerce-paypal-payments", "license": "GPL-2.0", diff --git a/readme.txt b/readme.txt index fe13ec28b..840fe2d93 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: woocommerce, paypal, payments, ecommerce, credit card Requires at least: 6.5 Tested up to: 6.8 Requires PHP: 7.4 -Stable tag: 3.0.6 +Stable tag: 3.0.7 License: GPLv2 License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -156,6 +156,22 @@ If you encounter issues with the PayPal buttons not appearing after an update, p == Changelog == += 3.0.7 - xxxx-xx-xx = +* Enhancement - Deprecate `application_context` in favor of `experience_context` object #3431 + **NOTE**: If you were extending/modifying the `application_context` object programmatically, you may need to update your code to utilize `experience_context` for your customizations. +* Enhancement - Add Contact Module feature +* Enhancement - Add WooCommerce Tracks integration +* Enhancement - Onboarding notification for Firefox browser #3433 +* Enhancement - Reset BN code on plugin uninstall #3471 +* Enhancement - Add "Stay updated with PayPal" option in the old and new settings UI #3430 +* Enhancement - Add French Territories to the supported ACDC countries list #3438 +* Enhancement - Auto-enable logging during onboarding #3369 +* Fix - DUPLICATE_INVOICE_ID in Sandbox due to missing invoice prefix #3435 +* Fix - Subscription product could not be unlinked from PayPal Subscription #3429 +* Fix - PayPal button greyed out on single product page for variable products with >2 attributes #3395 +* Fix - APMs automatically enabled despite selecting "No, ..." during onboarding #3362 +* Fix - Ditch items logic does not work when using saved card payment #3476 + = 3.0.6 - 2025-05-27 = * Enhancement - Implement 3D secure check for Google Pay #3163 * Enhancement - Add options for "Disable Credit Cards" and "Language" #3226 diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index 320a4f64a..b2cca9bc9 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce PayPal Payments * Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/ * Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage. - * Version: 3.0.6 + * Version: 3.0.7 * Author: PayPal * Author URI: https://paypal.com/ * License: GPL-2.0 @@ -11,7 +11,7 @@ * Requires Plugins: woocommerce * Requires at least: 6.5 * WC requires at least: 9.6 - * WC tested up to: 9.8 + * WC tested up to: 9.9 * Text Domain: woocommerce-paypal-payments * * @package WooCommerce\PayPalCommerce @@ -27,7 +27,7 @@ define( 'PAYPAL_API_URL', 'https://api-m.paypal.com' ); define( 'PAYPAL_URL', 'https://www.paypal.com' ); define( 'PAYPAL_SANDBOX_API_URL', 'https://api-m.sandbox.paypal.com' ); define( 'PAYPAL_SANDBOX_URL', 'https://www.sandbox.paypal.com' ); -define( 'PAYPAL_INTEGRATION_DATE', '2025-05-14' ); +define( 'PAYPAL_INTEGRATION_DATE', '2025-06-25' ); define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' ); ! defined( 'CONNECT_WOO_CLIENT_ID' ) && define( 'CONNECT_WOO_CLIENT_ID', 'AcCAsWta_JTL__OfpjspNyH7c1GGHH332fLwonA5CwX4Y10mhybRZmHLA0GdRbwKwjQIhpDQy0pluX_P' ); From f7f0f0f11fba2a448b303d99695a4430ecd87383 Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Wed, 25 Jun 2025 14:10:30 +0200 Subject: [PATCH 07/13] Add credit card test /w multiple products --- .../PHPUnit/IntegrationMockedTestCase.php | 26 ++- .../CreditcardTransactionTest.php | 184 ++++++++++++++++++ 2 files changed, 196 insertions(+), 14 deletions(-) create mode 100644 tests/integration/PHPUnit/Transaction_tests/CreditcardTransactionTest.php diff --git a/tests/integration/PHPUnit/IntegrationMockedTestCase.php b/tests/integration/PHPUnit/IntegrationMockedTestCase.php index 0d4028f51..815e86214 100644 --- a/tests/integration/PHPUnit/IntegrationMockedTestCase.php +++ b/tests/integration/PHPUnit/IntegrationMockedTestCase.php @@ -32,12 +32,9 @@ class IntegrationMockedTestCase extends TestCase public function setUp(): void { parent::setUp(); - //$this->default_product_id = $this->createAProductIfNotProvided(); $this->customer_id = $this->createCustomerIfNotExists(); - - $this->createTestProducts(); - //$this->createTestCoupons(); + $this->createTestCoupons(); } public function tearDown(): void @@ -211,20 +208,20 @@ class IntegrationMockedTestCase extends TestCase * @param bool $success Whether the order was successful * @return object The mocked OrderEndpoint */ - public function mockOrderEndpoint(string $intent = 'CAPTURE', bool $success = true): object + public function mockOrderEndpoint(string $intent = 'CAPTURE', bool $order_success = true, bool $capture_success = true): object { - $order_endpoint = \Mockery::mock(OrderEndpoint::class)->shouldIgnoreMissing(); + $order_endpoint = \Mockery::mock(OrderEndpoint::class); $order = \Mockery::mock(Order::class)->shouldIgnoreMissing(); $order->shouldReceive('id')->andReturn('TEST-ORDER-' . uniqid()); $order->shouldReceive('intent')->andReturn($intent); - $order_status = \Mockery::mock(OrderStatus::class)->shouldIgnoreMissing(); - $order_status->shouldReceive('is')->andReturn($success); - $order_status->shouldReceive('name')->andReturn($success ? 'COMPLETED' : 'FAILED'); + $order_status = \Mockery::mock(OrderStatus::class); + $order_status->shouldReceive('is')->andReturn($order_success); + $order_status->shouldReceive('name')->andReturn($order_success ? 'COMPLETED' : 'FAILED'); $order->shouldReceive('status')->andReturn($order_status); - $payment_source = \Mockery::mock(PaymentSource::class)->shouldIgnoreMissing(); + $payment_source = \Mockery::mock(PaymentSource::class); $payment_source->shouldReceive('name')->andReturn('card'); $order->shouldReceive('payment_source')->andReturn($payment_source); @@ -235,7 +232,8 @@ class IntegrationMockedTestCase extends TestCase $capture->shouldReceive('id')->andReturn('TEST-CAPTURE-' . uniqid()); $capture_status = \Mockery::mock(CaptureStatus::class)->shouldIgnoreMissing(); - $capture_status->shouldReceive('name')->andReturn($success ? 'COMPLETED' : 'DECLINED'); + $capture_status->shouldReceive('name')->andReturn($capture_success ? 'COMPLETED' : 'DECLINED'); + $capture_status->shouldReceive('details')->andReturn(null); $capture->shouldReceive('status')->andReturn($capture_status); // Mock authorizations for AUTHORIZE intent @@ -245,8 +243,8 @@ class IntegrationMockedTestCase extends TestCase $authorization->shouldReceive('id')->andReturn('TEST-AUTH-' . uniqid()); $auth_status = \Mockery::mock(\WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus::class)->shouldIgnoreMissing(); - $auth_status->shouldReceive('name')->andReturn($success ? 'CREATED' : 'DENIED'); - $auth_status->shouldReceive('is')->andReturn($success); + $auth_status->shouldReceive('name')->andReturn($capture_success ? 'CREATED' : 'DENIED'); + $auth_status->shouldReceive('is')->andReturn($capture_success); $authorization->shouldReceive('status')->andReturn($auth_status); $payments->shouldReceive('authorizations')->andReturn([$authorization]); $payments->shouldReceive('captures')->andReturn([]); @@ -267,7 +265,7 @@ class IntegrationMockedTestCase extends TestCase $order_endpoint->shouldReceive('capture')->andReturn($order); } $order_endpoint->shouldReceive('order')->andReturn($order); - + $order_endpoint->shouldReceive('patch_order_with')->andReturn($order); return $order_endpoint; } } diff --git a/tests/integration/PHPUnit/Transaction_tests/CreditcardTransactionTest.php b/tests/integration/PHPUnit/Transaction_tests/CreditcardTransactionTest.php new file mode 100644 index 000000000..76a3fdd55 --- /dev/null +++ b/tests/integration/PHPUnit/Transaction_tests/CreditcardTransactionTest.php @@ -0,0 +1,184 @@ +mockPaymentTokensEndpoint = \Mockery::mock(PaymentTokensEndpoint::class); + } + + /** + * Sets up a test container with common mocks + * + * @param OrderEndpoint $orderEndpoint + * @param array $additionalServices Additional services to override + * @return ContainerInterface + */ + protected function setupTestContainer(OrderEndpoint $orderEndpoint, array $additionalServices = []): ContainerInterface + { + $services = [ + 'api.endpoint.order' => function () use ($orderEndpoint) { + return $orderEndpoint; + }, + ]; + + return $this->bootstrapModule(array_merge($services, $additionalServices)); + } + + /** + * Creates a payment token and configures the mock endpoint to return it + * + * @param int $customer_id + * @param string $gateway_id + * @return WC_Payment_Token + */ + protected function setupPaymentToken(int $customer_id, string $gateway_id = PayPalGateway::ID): WC_Payment_Token + { + $paymentToken = $this->createAPaymentTokenForTheCustomer($customer_id, $gateway_id); + + $this->mockPaymentTokensEndpoint->shouldReceive('payment_tokens_for_customer') + ->andReturn([ + [ + 'id' => $paymentToken->get_token(), + 'payment_source' => new PaymentSource( + 'card', + (object)[ + 'last_digits' => $paymentToken->get_last4(), + 'brand' => $paymentToken->get_card_type(), + 'expiry' => $paymentToken->get_expiry_year() . '-' . $paymentToken->get_expiry_month() + ] + ) + ] + ]); + + return $paymentToken; + } + + /** + * Data provider for different product and discount combinations + */ + public function paymentProcessingDataProvider(): array + { + return [ + 'simple product only' => [ + 'products' => ['simple'], + 'discounts' => [], + 'expected_status' => 'processing' + ], + 'expensive product' => [ + 'products' => ['simple_expensive'], + 'discounts' => [], + 'expected_status' => 'processing' + ], + 'multiple products' => [ + 'products' => [ + ['preset' => 'simple', 'quantity' => 2], + 'simple_expensive' + ], + 'discounts' => [], + 'expected_status' => 'processing' + ],//TODO fix the discount logic is failing due to taxes + /*'simple product with percentage discount' => [ + 'products' => ['simple'], + 'discounts' => ['percentage_10'], + 'expected_status' => 'processing' + ], + 'simple product with fixed discount' => [ + 'products' => ['simple'], + 'discounts' => ['fixed_5'], + 'expected_status' => 'processing' + ],*/ + ]; + } + + /** + * Tests credit card payment processing with different product combinations. + * + * GIVEN a WooCommerce order with various product and discount combinations + * AND valid PayPal order ID in POST data + * AND valid credit card form data + * WHEN the payment is processed through the credit card gateway + * THEN the payment should be successfully captured + * AND the order status should change to the expected status + * AND a transaction ID should be set on the order + * + * @dataProvider paymentProcessingDataProvider + */ + public function testProcessPayment(array $products, array $discounts, string $expected_status) + { + // Mock successful PayPal API response + $mockOrderEndpoint = $this->mockOrderEndpoint('CAPTURE', false, true); + $this->setupTestContainer($mockOrderEndpoint); + + // Create order with provided products and discounts + $order = $this->getConfiguredOrder( + $this->customer_id, + 'ppcp-credit-card-gateway', + $products, + $discounts, + false + ); + + $paypal_order_id = 'TEST-PAYPAL-ORDER-' . uniqid(); + + // Set the PayPal order ID in POST data (simulating frontend submission) + $_POST['paypal_order_id'] = $paypal_order_id; + $order->update_meta_data('_paypal_order_id', $paypal_order_id); + $order->save(); + + // Mock the session handler to return null (forcing fallback to POST/meta) + $sessionHandler = \Mockery::mock(\WooCommerce\PayPalCommerce\Session\SessionHandler::class); + $sessionHandler->shouldReceive('order')->andReturn(null); + $sessionHandler->shouldReceive('destroy_session_data')->once(); + + // Add session handler to container overrides + $additionalServices = [ + 'session.handler' => function() use ($sessionHandler) { + return $sessionHandler; + } + ]; + + $c = $this->setupTestContainer($mockOrderEndpoint, $additionalServices); + + // Simulate credit card form data + $_POST['ppcp-credit-card-gateway-card-number'] = '4111111111111111'; + $_POST['ppcp-credit-card-gateway-card-expiry'] = '12/25'; + $_POST['ppcp-credit-card-gateway-card-cvc'] = '123'; + + // Get the gateway instance + $gateway = $c->get('wcgateway.credit-card-gateway'); + + // Process payment + $result = $gateway->process_payment($order->get_id()); + + // Assertions + $this->assertEquals('success', $result['result']); + $this->assertArrayHasKey('redirect', $result); + + // Verify order status changed + $order = wc_get_order($order->get_id()); // Refresh order + $this->assertEquals($expected_status, $order->get_status()); + $this->assertNotEmpty($order->get_transaction_id()); + + // Clean up POST data + unset($_POST['paypal_order_id']); + unset($_POST['ppcp-credit-card-gateway-card-number']); + unset($_POST['ppcp-credit-card-gateway-card-expiry']); + unset($_POST['ppcp-credit-card-gateway-card-cvc']); + } +} From e8a0950bfaad2be3de21ee6214cb61fe871609ba Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Wed, 25 Jun 2025 14:11:09 +0200 Subject: [PATCH 08/13] Update vaulting subscription tests --- tests/integration/PHPUnit/VaultingSubscriptionsTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/PHPUnit/VaultingSubscriptionsTest.php b/tests/integration/PHPUnit/VaultingSubscriptionsTest.php index 7958041a7..a54130135 100644 --- a/tests/integration/PHPUnit/VaultingSubscriptionsTest.php +++ b/tests/integration/PHPUnit/VaultingSubscriptionsTest.php @@ -182,7 +182,7 @@ class VaultingSubscriptionsTest extends IntegrationMockedTestCase */ public function test_renewal_payment_processing(string $gateway_id) { - $mockOrderEndpoint = $this->mockOrderEndpoint('CAPTURE', true); + $mockOrderEndpoint = $this->mockOrderEndpoint(); $c = $this->setupTestContainer($mockOrderEndpoint); $this->setupPaymentToken($this->customer_id, $gateway_id); $subscription = $this->createSubscription($this->customer_id, $gateway_id); @@ -204,7 +204,7 @@ class VaultingSubscriptionsTest extends IntegrationMockedTestCase */ public function test_renewal_handles_failed_payment() { - $mockOrderEndpoint = $this->mockOrderEndpoint('CAPTURE', false); + $mockOrderEndpoint = $this->mockOrderEndpoint('CAPTURE', false, false); $c = $this->setupTestContainer($mockOrderEndpoint); $this->setupPaymentToken($this->customer_id); $subscription = $this->createSubscription($this->customer_id, PayPalGateway::ID); @@ -226,7 +226,7 @@ class VaultingSubscriptionsTest extends IntegrationMockedTestCase public function test_authorize_only_subscription_renewal() { // Mock the OrderEndpoint with AUTHORIZE intent - $mockOrderEndpoint = $this->mockOrderEndpoint('AUTHORIZE', true); + $mockOrderEndpoint = $this->mockOrderEndpoint('AUTHORIZE', false, true); $c = $this->setupTestContainer($mockOrderEndpoint); // Setup payment token and subscription From c556db358a7e7b9d02df49b238b2a8d2013fc962 Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Wed, 25 Jun 2025 16:00:22 +0200 Subject: [PATCH 09/13] Create subscription products /w props --- .../PHPUnit/Factories/ProductFactory.php | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/tests/integration/PHPUnit/Factories/ProductFactory.php b/tests/integration/PHPUnit/Factories/ProductFactory.php index 50f74e1ba..5e3ac361b 100644 --- a/tests/integration/PHPUnit/Factories/ProductFactory.php +++ b/tests/integration/PHPUnit/Factories/ProductFactory.php @@ -113,34 +113,32 @@ class ProductFactory */ private function createSubscriptionProduct(array $preset): \WC_Product_Subscription { - $product = new \WC_Product_Subscription(); - $product_id = wc_get_product_id_by_sku($preset['sku']); - $product->set_id($product_id); - $product->set_name($preset['name']); - $product->set_regular_price($preset['price']); - $product->set_price($preset['price']); - $product->set_sku($preset['sku']); - $product->set_manage_stock(false); - $product->set_tax_status('taxable'); - $product->set_downloadable(false); - $product->set_virtual(false); - $product->set_stock_status('instock'); - $product->set_weight('1.1'); - // Subscription-specific properties - $product->set_subscription_period($preset['subscription_period']); - $product->set_subscription_period_interval($preset['subscription_period_interval']); - $product->set_subscription_length($preset['subscription_length']); - $product->set_subscription_trial_period($preset['subscription_trial_period']); - $product->set_subscription_trial_length($preset['subscription_trial_length']); - $product->set_subscription_price($preset['subscription_price']); - $product->set_subscription_sign_up_fee($preset['subscription_sign_up_fee']); + $product = new \WC_Product_Subscription(); + $product->set_props([ + 'name' => $preset['name'], + 'regular_price' => $preset['price'], + 'price' => $preset['price'], + 'sku' => $preset['sku'], + 'manage_stock' => false, + 'tax_status' => 'taxable', + 'downloadable' => false, + 'virtual' => false, + 'stock_status' => 'instock', + 'weight' => '1.1', + 'subscription_period' => $preset['subscription_period'], + 'subscription_period_interval' => $preset['subscription_period_interval'], + 'subscription_length' => $preset['subscription_length'], + 'subscription_trial_period' => $preset['subscription_trial_period'], + 'subscription_trial_length' => $preset['subscription_trial_length'], + 'subscription_price' => $preset['subscription_price'], + 'subscription_sign_up_fee' => $preset['subscription_sign_up_fee'], + ]); $product->set_status('publish'); $product->save(); - $this->created_product_ids[] = $product_id; - + $this->created_product_ids[] = $product->get_id(); return $product; } From e2c6dc350db19fe100ea847f197f5e1c034d5cd9 Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Thu, 26 Jun 2025 09:05:32 +0200 Subject: [PATCH 10/13] Create simple product if subs plugin is not there --- tests/integration/PHPUnit/Factories/ProductFactory.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/PHPUnit/Factories/ProductFactory.php b/tests/integration/PHPUnit/Factories/ProductFactory.php index 5e3ac361b..5cffdfe21 100644 --- a/tests/integration/PHPUnit/Factories/ProductFactory.php +++ b/tests/integration/PHPUnit/Factories/ProductFactory.php @@ -27,7 +27,6 @@ class ProductFactory $preset = $presets[$preset_name]; - // Check if product already exists by SKU if (isset($preset['sku'])) { $existing_product_id = wc_get_product_id_by_sku($preset['sku']); if ($existing_product_id) { @@ -38,7 +37,6 @@ class ProductFactory } } - // Check if product already exists by ID if (isset($preset['product_id'])) { $existing_product = wc_get_product($preset['product_id']); if ($existing_product) { @@ -46,6 +44,9 @@ class ProductFactory } } + if ($preset['type'] === 'subscription' && !class_exists('\WC_Product_Subscription')) { + return $this->createSimpleProduct($preset); + } switch ($preset['type']) { case 'variable': return $this->createVariableProduct($preset); @@ -113,7 +114,6 @@ class ProductFactory */ private function createSubscriptionProduct(array $preset): \WC_Product_Subscription { - $product = new \WC_Product_Subscription(); $product->set_props([ 'name' => $preset['name'], From 846a77d1440fc09a1625a9cb7a23d78d4d77666a Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Thu, 26 Jun 2025 09:10:37 +0200 Subject: [PATCH 11/13] Throw if the product cannot be found --- .../PHPUnit/Factories/OrderFactory.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/integration/PHPUnit/Factories/OrderFactory.php b/tests/integration/PHPUnit/Factories/OrderFactory.php index 8e9c6c5f2..c21f20562 100644 --- a/tests/integration/PHPUnit/Factories/OrderFactory.php +++ b/tests/integration/PHPUnit/Factories/OrderFactory.php @@ -86,8 +86,21 @@ class OrderFactory private function addProductsToOrder(WC_Order $order, array $products): void { foreach ($products as $product_data) { - $product_sku = $product_data['sku']; - $product_id = wc_get_product_id_by_sku($product_sku); + $product_id = null; + + if (!empty($product_data['sku'])) { + $product_sku = $product_data['sku']; + $product_id = wc_get_product_id_by_sku($product_sku); + } + + if (!$product_id && isset($product_data['id'])) { + $product_id = $product_data['id']; + } + + if (!$product_id) { + throw new \WC_Data_Exception('invalid_product', "Product not found - no valid SKU or ID provided"); + } + $variation_id = $product_data['variation_id'] ?? 0; $product_type = $product_data['type'] ?? 'simple'; From 2280cf9081658d6c348c6e7d551a88b62ec021a5 Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Thu, 26 Jun 2025 09:38:27 +0200 Subject: [PATCH 12/13] Fix vaulting integration credit card test --- tests/integration/PHPUnit/IntegrationMockedTestCase.php | 6 +++++- tests/integration/PHPUnit/VaultingSubscriptionsTest.php | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/integration/PHPUnit/IntegrationMockedTestCase.php b/tests/integration/PHPUnit/IntegrationMockedTestCase.php index 815e86214..8227a444f 100644 --- a/tests/integration/PHPUnit/IntegrationMockedTestCase.php +++ b/tests/integration/PHPUnit/IntegrationMockedTestCase.php @@ -220,9 +220,13 @@ class IntegrationMockedTestCase extends TestCase $order_status->shouldReceive('is')->andReturn($order_success); $order_status->shouldReceive('name')->andReturn($order_success ? 'COMPLETED' : 'FAILED'); $order->shouldReceive('status')->andReturn($order_status); - + $card_properties = new \stdClass(); + $card_properties->brand = 'VISA'; + $card_properties->last_digits = '1234'; + $card_properties->expiry = '2026-12'; $payment_source = \Mockery::mock(PaymentSource::class); $payment_source->shouldReceive('name')->andReturn('card'); + $payment_source->shouldReceive('properties')->andReturn($card_properties); $order->shouldReceive('payment_source')->andReturn($payment_source); $purchase_unit = \Mockery::mock(PurchaseUnit::class)->shouldIgnoreMissing(); diff --git a/tests/integration/PHPUnit/VaultingSubscriptionsTest.php b/tests/integration/PHPUnit/VaultingSubscriptionsTest.php index a54130135..c61cb8253 100644 --- a/tests/integration/PHPUnit/VaultingSubscriptionsTest.php +++ b/tests/integration/PHPUnit/VaultingSubscriptionsTest.php @@ -167,7 +167,7 @@ class VaultingSubscriptionsTest extends IntegrationMockedTestCase { return [ 'PayPal Gateway' => [PayPalGateway::ID], - //'Credit Card Gateway' => [CreditCardGateway::ID] + 'Credit Card Gateway' => [CreditCardGateway::ID] ]; } From 4accd33e1f5c99854f96539ab6bf66d72f403fd9 Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Thu, 26 Jun 2025 10:37:27 +0200 Subject: [PATCH 13/13] Pass code style --- .../PHPUnit/Factories/CouponFactory.php | 134 +++---- .../PHPUnit/Factories/OrderFactory.php | 364 +++++++++--------- .../PHPUnit/Factories/ProductFactory.php | 224 +++++------ .../PHPUnit/Fixtures/DiscountPresets.php | 36 +- .../PHPUnit/Fixtures/ProductPresets.php | 82 ++-- .../PHPUnit/IntegrationMockedTestCase.php | 352 ++++++++--------- .../PHPUnit/Traits/CleansTestData.php | 30 +- .../PHPUnit/Traits/CreateTestOrders.php | 67 ++-- .../PHPUnit/Traits/CreateTestProducts.php | 84 ++-- .../PHPUnit/VaultingSubscriptionsTest.php | 284 +++++++------- 10 files changed, 831 insertions(+), 826 deletions(-) diff --git a/tests/integration/PHPUnit/Factories/CouponFactory.php b/tests/integration/PHPUnit/Factories/CouponFactory.php index 6091368ad..b1b53cd30 100644 --- a/tests/integration/PHPUnit/Factories/CouponFactory.php +++ b/tests/integration/PHPUnit/Factories/CouponFactory.php @@ -8,85 +8,85 @@ use WooCommerce\PayPalCommerce\Tests\Integration\Fixtures\DiscountPresets; class CouponFactory { - private array $created_coupon_ids = []; + private array $created_coupon_ids = []; - /** - * @param string $preset_name - * @return WC_Coupon - * @throws \WC_Data_Exception - */ - public function createFromPreset(string $preset_name): WC_Coupon - { - $presets = DiscountPresets::get(); + /** + * @param string $preset_name + * @return WC_Coupon + * @throws \WC_Data_Exception + */ + public function createFromPreset(string $preset_name): WC_Coupon + { + $presets = DiscountPresets::get(); - if (!isset($presets[$preset_name])) { - throw new \WC_Data_Exception('invalid_preset', "Coupon preset '{$preset_name}' not found"); - } + if (!isset($presets[$preset_name])) { + throw new \WC_Data_Exception('invalid_preset', "Coupon preset '{$preset_name}' not found"); + } - $preset = $presets[$preset_name]; + $preset = $presets[$preset_name]; - if (!isset($preset['coupon_code'])) { - throw new \WC_Data_Exception('invalid_preset', "Preset '{$preset_name}' is not a coupon"); - } + if (!isset($preset['coupon_code'])) { + throw new \WC_Data_Exception('invalid_preset', "Preset '{$preset_name}' is not a coupon"); + } - return $this->createCoupon($preset); - } + return $this->createCoupon($preset); + } - /** - * @param array $preset - * @return WC_Coupon - */ - private function createCoupon(array $preset): WC_Coupon - { - $coupon = new WC_Coupon(); - $coupon->set_code($preset['coupon_code']); - $coupon->set_discount_type($preset['type']); - $coupon->set_amount($preset['amount']); - $coupon->set_status('publish'); - $coupon->save(); + /** + * @param array $preset + * @return WC_Coupon + */ + private function createCoupon(array $preset): WC_Coupon + { + $coupon = new WC_Coupon(); + $coupon->set_code($preset['coupon_code']); + $coupon->set_discount_type($preset['type']); + $coupon->set_amount($preset['amount']); + $coupon->set_status('publish'); + $coupon->save(); - $this->created_coupon_ids[] = $coupon->get_id(); + $this->created_coupon_ids[] = $coupon->get_id(); - return $coupon; - } + return $coupon; + } - /** - * @param string $coupon_code - * @return bool - */ - public function exists(string $coupon_code): bool - { - return (bool) wc_get_coupon_id_by_code($coupon_code); - } + /** + * @param string $coupon_code + * @return bool + */ + public function exists(string $coupon_code): bool + { + return (bool)wc_get_coupon_id_by_code($coupon_code); + } - /** - * @param string $coupon_code - * @return WC_Coupon|null - */ - public function getByCode(string $coupon_code): ?WC_Coupon - { - $coupon_id = wc_get_coupon_id_by_code($coupon_code); + /** + * @param string $coupon_code + * @return WC_Coupon|null + */ + public function getByCode(string $coupon_code): ?WC_Coupon + { + $coupon_id = wc_get_coupon_id_by_code($coupon_code); - return $coupon_id ? new WC_Coupon($coupon_id) : null; - } + return $coupon_id ? new WC_Coupon($coupon_id) : null; + } - /** - * Delete all created coupons - */ - public function cleanup(): void - { - foreach ($this->created_coupon_ids as $coupon_id) { - wp_delete_post($coupon_id, true); - } + /** + * Delete all created coupons + */ + public function cleanup(): void + { + foreach ($this->created_coupon_ids as $coupon_id) { + wp_delete_post($coupon_id, true); + } - $this->created_coupon_ids = []; - } + $this->created_coupon_ids = []; + } - /** - * @return array - */ - public function getCreatedIds(): array - { - return $this->created_coupon_ids; - } + /** + * @return array + */ + public function getCreatedIds(): array + { + return $this->created_coupon_ids; + } } diff --git a/tests/integration/PHPUnit/Factories/OrderFactory.php b/tests/integration/PHPUnit/Factories/OrderFactory.php index c21f20562..ebeaca71b 100644 --- a/tests/integration/PHPUnit/Factories/OrderFactory.php +++ b/tests/integration/PHPUnit/Factories/OrderFactory.php @@ -11,81 +11,83 @@ use WooCommerce\PayPalCommerce\Tests\Integration\Fixtures\DiscountPresets; class OrderFactory { - private array $created_order_ids = []; - private ProductFactory $product_factory; - private CouponFactory $coupon_factory; + private array $created_order_ids = []; + private ProductFactory $product_factory; + private CouponFactory $coupon_factory; - public function __construct( - ProductFactory $product_factory = null, - CouponFactory $coupon_factory = null - ) { - $this->product_factory = $product_factory ?? new ProductFactory(); - $this->coupon_factory = $coupon_factory ?? new CouponFactory(); - } + public function __construct( + ProductFactory $product_factory = null, + CouponFactory $coupon_factory = null + ) + { + $this->product_factory = $product_factory ?? new ProductFactory(); + $this->coupon_factory = $coupon_factory ?? new CouponFactory(); + } - /** - * @param int $customer_id - * @param string $payment_method - * @param array $product_presets - * @param array $discount_presets - * @param bool $set_paid - * @return WC_Order - * @throws \WC_Data_Exception - */ - public function create( - int $customer_id, - string $payment_method, - array $product_presets, - array $discount_presets = [], - bool $set_paid = true - ): WC_Order { - $products = $this->resolveProductPresets($product_presets); - $discounts = $this->resolveDiscountPresets($discount_presets); + /** + * @param int $customer_id + * @param string $payment_method + * @param array $product_presets + * @param array $discount_presets + * @param bool $set_paid + * @return WC_Order + * @throws \WC_Data_Exception + */ + public function create( + int $customer_id, + string $payment_method, + array $product_presets, + array $discount_presets = [], + bool $set_paid = true + ): WC_Order + { + $products = $this->resolveProductPresets($product_presets); + $discounts = $this->resolveDiscountPresets($discount_presets); - $order = wc_create_order([ - 'customer_id' => $customer_id, - 'set_paid' => $set_paid, - ]); + $order = wc_create_order([ + 'customer_id' => $customer_id, + 'set_paid' => $set_paid, + ]); - if (is_wp_error($order)) { - throw new \WC_Data_Exception('order_creation_failed', 'Failed to create order'); - } + if (is_wp_error($order)) { + throw new \WC_Data_Exception('order_creation_failed', 'Failed to create order'); + } - $this->setBillingAddress($order); - $this->addProductsToOrder($order, $products); - $this->applyDiscountsToOrder($order, $discounts); + $this->setBillingAddress($order); + $this->addProductsToOrder($order, $products); + $this->applyDiscountsToOrder($order, $discounts); - $order->set_payment_method($payment_method); - $order->calculate_totals(); - $order->save(); + $order->set_payment_method($payment_method); + $order->calculate_totals(); + $order->save(); - return $order; - } + return $order; + } - /** - * @param WC_Order $order - */ - private function setBillingAddress(WC_Order $order): void - { - $order->set_billing_first_name('John'); - $order->set_billing_last_name('Doe'); - $order->set_billing_address_1('969 Market'); - $order->set_billing_city('San Francisco'); - $order->set_billing_state('CA'); - $order->set_billing_postcode('94103'); - $order->set_billing_country('US'); - $order->set_billing_email('john.doe@example.com'); - $order->set_billing_phone('(555) 555-5555'); - } + /** + * @param WC_Order $order + */ + private function setBillingAddress(WC_Order $order): void + { + $order->set_billing_first_name('John'); + $order->set_billing_last_name('Doe'); + $order->set_billing_address_1('969 Market'); + $order->set_billing_city('San Francisco'); + $order->set_billing_state('CA'); + $order->set_billing_postcode('94103'); + $order->set_billing_country('US'); + $order->set_billing_email('john.doe@example.com'); + $order->set_billing_phone('(555) 555-5555'); + } - /** - * @param WC_Order $order - * @param array $products - * @throws \WC_Data_Exception - */ - private function addProductsToOrder(WC_Order $order, array $products): void - { - foreach ($products as $product_data) { + /** + * @param WC_Order $order + * @param array $products + * @throws \WC_Data_Exception + */ + private function addProductsToOrder(WC_Order $order, array $products): void + { + foreach ($products as $product_data) { $product_id = null; if (!empty($product_data['sku'])) { @@ -101,140 +103,140 @@ class OrderFactory throw new \WC_Data_Exception('invalid_product', "Product not found - no valid SKU or ID provided"); } - $variation_id = $product_data['variation_id'] ?? 0; - $product_type = $product_data['type'] ?? 'simple'; + $variation_id = $product_data['variation_id'] ?? 0; + $product_type = $product_data['type'] ?? 'simple'; - $product = wc_get_product($variation_id ?: $product_id); + $product = wc_get_product($variation_id ?: $product_id); - if (!$product) { - throw new \WC_Data_Exception('invalid_product', "Product {$product_id} not found"); - } + if (!$product) { + throw new \WC_Data_Exception('invalid_product', "Product {$product_id} not found"); + } - // Use appropriate item class based on product type - $item = $this->createOrderItem($product_type, $product_data, $product); + // Use appropriate item class based on product type + $item = $this->createOrderItem($product_type, $product_data, $product); - if ($variation_id && $product->is_type('variation')) { - $item->set_variation_data($product->get_variation_attributes()); - } + if ($variation_id && $product->is_type('variation')) { + $item->set_variation_data($product->get_variation_attributes()); + } - $order->add_item($item); - } - } + $order->add_item($item); + } + } - /** - * @param WC_Order $order - * @param array $discounts - */ - private function applyDiscountsToOrder(WC_Order $order, array $discounts): void - { - foreach ($discounts as $discount) { - if (isset($discount['coupon_code'])) { - $order->apply_coupon($discount['coupon_code']); - } + /** + * @param WC_Order $order + * @param array $discounts + */ + private function applyDiscountsToOrder(WC_Order $order, array $discounts): void + { + foreach ($discounts as $discount) { + if (isset($discount['coupon_code'])) { + $order->apply_coupon($discount['coupon_code']); + } - if (isset($discount['fee'])) { - $fee = new WC_Order_Item_Fee(); - $fee->set_props([ - 'name' => $discount['fee']['name'], - 'amount' => -abs($discount['fee']['amount']), - 'total' => -abs($discount['fee']['amount']), - ]); - $order->add_item($fee); - } - } - } + if (isset($discount['fee'])) { + $fee = new WC_Order_Item_Fee(); + $fee->set_props([ + 'name' => $discount['fee']['name'], + 'amount' => -abs($discount['fee']['amount']), + 'total' => -abs($discount['fee']['amount']), + ]); + $order->add_item($fee); + } + } + } - /** - * @param array $product_presets - * @return array - * @throws \WC_Data_Exception - */ - private function resolveProductPresets(array $product_presets): array - { - $available_presets = ProductPresets::get(); - $products = []; + /** + * @param array $product_presets + * @return array + * @throws \WC_Data_Exception + */ + private function resolveProductPresets(array $product_presets): array + { + $available_presets = ProductPresets::get(); + $products = []; - foreach ($product_presets as $preset) { - if (is_string($preset)) { - if (!isset($available_presets[$preset])) { - throw new \WC_Data_Exception('invalid_preset', "Product preset '{$preset}' not found"); - } - $products[] = $available_presets[$preset]; - } elseif (is_array($preset)) { - $preset_name = $preset['preset']; - $quantity = $preset['quantity'] ?? 1; + foreach ($product_presets as $preset) { + if (is_string($preset)) { + if (!isset($available_presets[$preset])) { + throw new \WC_Data_Exception('invalid_preset', "Product preset '{$preset}' not found"); + } + $products[] = $available_presets[$preset]; + } elseif (is_array($preset)) { + $preset_name = $preset['preset']; + $quantity = $preset['quantity'] ?? 1; - if (!isset($available_presets[$preset_name])) { - throw new \WC_Data_Exception('invalid_preset', "Product preset '{$preset_name}' not found"); - } + if (!isset($available_presets[$preset_name])) { + throw new \WC_Data_Exception('invalid_preset', "Product preset '{$preset_name}' not found"); + } - $product_data = $available_presets[$preset_name]; - $product_data['quantity'] = $quantity; - $products[] = $product_data; - } - } + $product_data = $available_presets[$preset_name]; + $product_data['quantity'] = $quantity; + $products[] = $product_data; + } + } - return $products; - } + return $products; + } - /** - * @param array $discount_presets - * @return array - * @throws \WC_Data_Exception - */ - private function resolveDiscountPresets(array $discount_presets): array - { - $available_presets = DiscountPresets::get(); - $discounts = []; + /** + * @param array $discount_presets + * @return array + * @throws \WC_Data_Exception + */ + private function resolveDiscountPresets(array $discount_presets): array + { + $available_presets = DiscountPresets::get(); + $discounts = []; - foreach ($discount_presets as $preset) { - if (!isset($available_presets[$preset])) { - throw new \WC_Data_Exception('invalid_preset', "Discount preset '{$preset}' not found"); - } - $discounts[] = $available_presets[$preset]; - } + foreach ($discount_presets as $preset) { + if (!isset($available_presets[$preset])) { + throw new \WC_Data_Exception('invalid_preset', "Discount preset '{$preset}' not found"); + } + $discounts[] = $available_presets[$preset]; + } - return $discounts; - } + return $discounts; + } - /** - * Delete all created orders - */ - public function cleanup(): void - { - foreach ($this->created_order_ids as $order_id) { - wp_delete_post($order_id, true); - } + /** + * Delete all created orders + */ + public function cleanup(): void + { + foreach ($this->created_order_ids as $order_id) { + wp_delete_post($order_id, true); + } - $this->created_order_ids = []; - } + $this->created_order_ids = []; + } - /** - * @return array - */ - public function getCreatedIds(): array - { - return $this->created_order_ids; - } + /** + * @return array + */ + public function getCreatedIds(): array + { + return $this->created_order_ids; + } - /** - * @param string $product_type - * @param array $product_data - * @param \WC_Product $product - * @return \WC_Order_Item_Product - */ - private function createOrderItem(string $product_type, array $product_data, \WC_Product $product): \WC_Order_Item_Product - { - $item = new \WC_Order_Item_Product(); + /** + * @param string $product_type + * @param array $product_data + * @param \WC_Product $product + * @return \WC_Order_Item_Product + */ + private function createOrderItem(string $product_type, array $product_data, \WC_Product $product): \WC_Order_Item_Product + { + $item = new \WC_Order_Item_Product(); - $item->set_props([ - 'product_id' => $product->get_id(), - 'variation_id' => $product_data['variation_id'] ?? 0, - 'quantity' => $product_data['quantity'], - 'subtotal' => $product->get_price() * $product_data['quantity'], - 'total' => $product->get_price() * $product_data['quantity'], - ]); + $item->set_props([ + 'product_id' => $product->get_id(), + 'variation_id' => $product_data['variation_id'] ?? 0, + 'quantity' => $product_data['quantity'], + 'subtotal' => $product->get_price() * $product_data['quantity'], + 'total' => $product->get_price() * $product_data['quantity'], + ]); - return $item; - } + return $item; + } } diff --git a/tests/integration/PHPUnit/Factories/ProductFactory.php b/tests/integration/PHPUnit/Factories/ProductFactory.php index 5cffdfe21..caaaabde3 100644 --- a/tests/integration/PHPUnit/Factories/ProductFactory.php +++ b/tests/integration/PHPUnit/Factories/ProductFactory.php @@ -10,111 +10,111 @@ use WooCommerce\PayPalCommerce\Tests\Integration\Fixtures\ProductPresets; class ProductFactory { - private array $created_product_ids = []; + private array $created_product_ids = []; - /** - * @param string $preset_name - * @return \WC_Product - * @throws \WC_Data_Exception - */ - public function createFromPreset(string $preset_name): \WC_Product - { - $presets = ProductPresets::get(); + /** + * @param string $preset_name + * @return \WC_Product + * @throws \WC_Data_Exception + */ + public function createFromPreset(string $preset_name): \WC_Product + { + $presets = ProductPresets::get(); - if (!isset($presets[$preset_name])) { - throw new \WC_Data_Exception('invalid_preset', "Product preset '{$preset_name}' not found"); - } + if (!isset($presets[$preset_name])) { + throw new \WC_Data_Exception('invalid_preset', "Product preset '{$preset_name}' not found"); + } - $preset = $presets[$preset_name]; + $preset = $presets[$preset_name]; - if (isset($preset['sku'])) { - $existing_product_id = wc_get_product_id_by_sku($preset['sku']); - if ($existing_product_id) { - $existing_product = wc_get_product($existing_product_id); - if ($existing_product) { - return $existing_product; - } - } - } + if (isset($preset['sku'])) { + $existing_product_id = wc_get_product_id_by_sku($preset['sku']); + if ($existing_product_id) { + $existing_product = wc_get_product($existing_product_id); + if ($existing_product) { + return $existing_product; + } + } + } - if (isset($preset['product_id'])) { - $existing_product = wc_get_product($preset['product_id']); - if ($existing_product) { - return $existing_product; - } - } + if (isset($preset['product_id'])) { + $existing_product = wc_get_product($preset['product_id']); + if ($existing_product) { + return $existing_product; + } + } if ($preset['type'] === 'subscription' && !class_exists('\WC_Product_Subscription')) { return $this->createSimpleProduct($preset); } - switch ($preset['type']) { - case 'variable': - return $this->createVariableProduct($preset); - case 'subscription': - return $this->createSubscriptionProduct($preset); - case 'simple': - default: - return $this->createSimpleProduct($preset); - } - } + switch ($preset['type']) { + case 'variable': + return $this->createVariableProduct($preset); + case 'subscription': + return $this->createSubscriptionProduct($preset); + case 'simple': + default: + return $this->createSimpleProduct($preset); + } + } - /** - * @param array $preset - * @return WC_Product_Simple - */ - private function createSimpleProduct(array $preset): WC_Product_Simple - { - $product = new WC_Product_Simple(); - $product_id = wc_get_product_id_by_sku($preset['sku']); - $product->set_sku($preset['sku']); - $product->set_id($product_id); - $product->set_name($preset['name']); - $product->set_regular_price($preset['price']); - $product->set_status('publish'); - $product->save(); + /** + * @param array $preset + * @return WC_Product_Simple + */ + private function createSimpleProduct(array $preset): WC_Product_Simple + { + $product = new WC_Product_Simple(); + $product_id = wc_get_product_id_by_sku($preset['sku']); + $product->set_sku($preset['sku']); + $product->set_id($product_id); + $product->set_name($preset['name']); + $product->set_regular_price($preset['price']); + $product->set_status('publish'); + $product->save(); - $this->created_product_ids[] = $product_id; + $this->created_product_ids[] = $product_id; - return $product; - } + return $product; + } - /** - * @param array $preset - * @return WC_Product_Variation - */ - private function createVariableProduct(array $preset): WC_Product_Variation - { - // Create parent variable product - $parent = new WC_Product_Variable(); - $product_id = wc_get_product_id_by_sku($preset['sku']); - $parent->set_sku($preset['sku']); - $parent->set_id($product_id); - $parent->set_name($preset['name']); - $parent->set_status('publish'); - $parent->save(); + /** + * @param array $preset + * @return WC_Product_Variation + */ + private function createVariableProduct(array $preset): WC_Product_Variation + { + // Create parent variable product + $parent = new WC_Product_Variable(); + $product_id = wc_get_product_id_by_sku($preset['sku']); + $parent->set_sku($preset['sku']); + $parent->set_id($product_id); + $parent->set_name($preset['name']); + $parent->set_status('publish'); + $parent->save(); - // Create variation - $variation = new WC_Product_Variation(); - $variation->set_id($preset['variation_id']); - $variation->set_parent_id($preset['product_id']); - $variation->set_regular_price($preset['price']); - $variation->set_attributes(['color' => 'red']); - $variation->set_status('publish'); - $variation->save(); + // Create variation + $variation = new WC_Product_Variation(); + $variation->set_id($preset['variation_id']); + $variation->set_parent_id($preset['product_id']); + $variation->set_regular_price($preset['price']); + $variation->set_attributes(['color' => 'red']); + $variation->set_status('publish'); + $variation->save(); - $this->created_product_ids[] = $product_id; - $this->created_product_ids[] = $preset['variation_id']; + $this->created_product_ids[] = $product_id; + $this->created_product_ids[] = $preset['variation_id']; - return $variation; - } + return $variation; + } - /** - * @param array $preset - * @return \WC_Product_Subscription - */ - private function createSubscriptionProduct(array $preset): \WC_Product_Subscription - { - $product = new \WC_Product_Subscription(); + /** + * @param array $preset + * @return \WC_Product_Subscription + */ + private function createSubscriptionProduct(array $preset): \WC_Product_Subscription + { + $product = new \WC_Product_Subscription(); $product->set_props([ 'name' => $preset['name'], 'regular_price' => $preset['price'], @@ -135,32 +135,32 @@ class ProductFactory 'subscription_sign_up_fee' => $preset['subscription_sign_up_fee'], ]); - $product->set_status('publish'); - $product->save(); + $product->set_status('publish'); + $product->save(); - $this->created_product_ids[] = $product->get_id(); - return $product; - } + $this->created_product_ids[] = $product->get_id(); + return $product; + } - /** - * @param string $sku - * @return bool - */ - public function exists(string $sku): bool - { - $existing_product_id = wc_get_product_id_by_sku($sku); - return (bool) $existing_product_id; - } + /** + * @param string $sku + * @return bool + */ + public function exists(string $sku): bool + { + $existing_product_id = wc_get_product_id_by_sku($sku); + return (bool)$existing_product_id; + } - /** - * Delete all created products - */ - public function cleanup(): void - { - foreach ($this->created_product_ids as $product_id) { - wp_delete_post($product_id, true); - } + /** + * Delete all created products + */ + public function cleanup(): void + { + foreach ($this->created_product_ids as $product_id) { + wp_delete_post($product_id, true); + } - $this->created_product_ids = []; - } + $this->created_product_ids = []; + } } diff --git a/tests/integration/PHPUnit/Fixtures/DiscountPresets.php b/tests/integration/PHPUnit/Fixtures/DiscountPresets.php index 019bf1411..c0a4e2320 100644 --- a/tests/integration/PHPUnit/Fixtures/DiscountPresets.php +++ b/tests/integration/PHPUnit/Fixtures/DiscountPresets.php @@ -5,22 +5,22 @@ namespace WooCommerce\PayPalCommerce\Tests\Integration\Fixtures; class DiscountPresets { - public static function get(): array - { - return [ - 'percentage_10' => [ - 'coupon_code' => 'TEST10PERCENT', - 'type' => 'percent', - 'amount' => '10' - ], - 'fixed_5' => [ - 'coupon_code' => 'TEST5FIXED', - 'type' => 'fixed_cart', - 'amount' => '5.00' - ], - 'manual_discount' => [ - 'fee' => ['name' => 'Test Discount', 'amount' => 3.50] - ], - ]; - } + public static function get(): array + { + return [ + 'percentage_10' => [ + 'coupon_code' => 'TEST10PERCENT', + 'type' => 'percent', + 'amount' => '10' + ], + 'fixed_5' => [ + 'coupon_code' => 'TEST5FIXED', + 'type' => 'fixed_cart', + 'amount' => '5.00' + ], + 'manual_discount' => [ + 'fee' => ['name' => 'Test Discount', 'amount' => 3.50] + ], + ]; + } } diff --git a/tests/integration/PHPUnit/Fixtures/ProductPresets.php b/tests/integration/PHPUnit/Fixtures/ProductPresets.php index 581b37b2a..3a447ba27 100644 --- a/tests/integration/PHPUnit/Fixtures/ProductPresets.php +++ b/tests/integration/PHPUnit/Fixtures/ProductPresets.php @@ -5,45 +5,45 @@ namespace WooCommerce\PayPalCommerce\Tests\Integration\Fixtures; class ProductPresets { - public static function get(): array - { - return [ - 'simple' => [ - 'sku' => 'DUMMY_SIMPLE_SKU_01', - 'name' => 'Test Simple Product', - 'price' => '10.00', - 'quantity' => 1, - 'type' => 'simple' - ], - 'simple_expensive' => [ - 'sku' => 'DUMMY_SIMPLE_SKU_02', - 'name' => 'Test Expensive Product', - 'price' => '199.99', - 'quantity' => 1, - 'type' => 'simple' - ], - /*'variable' => [ - 'sku' => 'DUMMY_VARIABLE_SKU_01', - 'variation_id' => 20002, - 'name' => 'Test Variable Product', - 'price' => '25.00', - 'quantity' => 1, - 'type' => 'variable' - ],*/ - 'subscription' => [ - 'name' => 'Dummy Subscription Product', - 'price' => '10.00', - 'quantity' => 1, - 'type' => 'subscription', - 'sku' => 'DUMMY SUB SKU', - 'subscription_period' => 'day', - 'subscription_period_interval' => 1, - 'subscription_length' => 0, - 'subscription_trial_period' => '', - 'subscription_trial_length' => 0, - 'subscription_price' => 10, - 'subscription_sign_up_fee' => 0, - ] - ]; - } + public static function get(): array + { + return [ + 'simple' => [ + 'sku' => 'DUMMY_SIMPLE_SKU_01', + 'name' => 'Test Simple Product', + 'price' => '10.00', + 'quantity' => 1, + 'type' => 'simple' + ], + 'simple_expensive' => [ + 'sku' => 'DUMMY_SIMPLE_SKU_02', + 'name' => 'Test Expensive Product', + 'price' => '199.99', + 'quantity' => 1, + 'type' => 'simple' + ], + /*'variable' => [ + 'sku' => 'DUMMY_VARIABLE_SKU_01', + 'variation_id' => 20002, + 'name' => 'Test Variable Product', + 'price' => '25.00', + 'quantity' => 1, + 'type' => 'variable' + ],*/ + 'subscription' => [ + 'name' => 'Dummy Subscription Product', + 'price' => '10.00', + 'quantity' => 1, + 'type' => 'subscription', + 'sku' => 'DUMMY SUB SKU', + 'subscription_period' => 'day', + 'subscription_period_interval' => 1, + 'subscription_length' => 0, + 'subscription_trial_period' => '', + 'subscription_trial_length' => 0, + 'subscription_price' => 10, + 'subscription_sign_up_fee' => 0, + ] + ]; + } } diff --git a/tests/integration/PHPUnit/IntegrationMockedTestCase.php b/tests/integration/PHPUnit/IntegrationMockedTestCase.php index 8227a444f..8394f1179 100644 --- a/tests/integration/PHPUnit/IntegrationMockedTestCase.php +++ b/tests/integration/PHPUnit/IntegrationMockedTestCase.php @@ -27,15 +27,15 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; class IntegrationMockedTestCase extends TestCase { - use MockeryPHPUnitIntegration, CreateTestOrders, CreateTestProducts; + use MockeryPHPUnitIntegration, CreateTestOrders, CreateTestProducts; - public function setUp(): void - { - parent::setUp(); + public function setUp(): void + { + parent::setUp(); $this->customer_id = $this->createCustomerIfNotExists(); $this->createTestProducts(); $this->createTestCoupons(); - } + } public function tearDown(): void { @@ -44,149 +44,149 @@ class IntegrationMockedTestCase extends TestCase parent::tearDown(); } - /** - * @param array $overriddenServices - * @return ContainerInterface - */ - protected function bootstrapModule(array $overriddenServices = []): ContainerInterface - { - $overriddenServices = array_merge([ - 'http.redirector' => function () { - return new RedirectorStub(); - } - ], $overriddenServices); + /** + * @param array $overriddenServices + * @return ContainerInterface + */ + protected function bootstrapModule(array $overriddenServices = []): ContainerInterface + { + $overriddenServices = array_merge([ + 'http.redirector' => function () { + return new RedirectorStub(); + } + ], $overriddenServices); - $module = new class ($overriddenServices) implements ServiceModule, ExecutableModule { - use ModuleClassNameIdTrait; + $module = new class ($overriddenServices) implements ServiceModule, ExecutableModule { + use ModuleClassNameIdTrait; - public function __construct(array $services) - { - $this->services = $services; - } + public function __construct(array $services) + { + $this->services = $services; + } - public function services(): array - { - return $this->services; - } + public function services(): array + { + return $this->services; + } - public function run(ContainerInterface $c): bool - { - return true; - } - }; + public function run(ContainerInterface $c): bool + { + return true; + } + }; - $rootDir = ROOT_DIR; - $bootstrap = require("$rootDir/bootstrap.php"); - $appContainer = $bootstrap($rootDir, [], [$module]); + $rootDir = ROOT_DIR; + $bootstrap = require("$rootDir/bootstrap.php"); + $appContainer = $bootstrap($rootDir, [], [$module]); - PPCP::init($appContainer); + PPCP::init($appContainer); - return $appContainer; - } + return $appContainer; + } - public function createCustomerIfNotExists(int $customer_id= 1): int - { - $customer = new \WC_Customer($customer_id); - if ( empty($customer->get_email() )) { - $customer->set_email('customer'. $customer_id. '@example.com'); + public function createCustomerIfNotExists(int $customer_id = 1): int + { + $customer = new \WC_Customer($customer_id); + if (empty($customer->get_email())) { + $customer->set_email('customer' . $customer_id . '@example.com'); $customer->set_first_name('John'); $customer->set_last_name('Doe'); $customer->save(); } - return $customer->get_id(); - } + return $customer->get_id(); + } - /** - * Creates a payment token for a customer. - * - * @param int $customer_id The customer ID. - * @return WC_Payment_Token_CC The created payment token. - * @throws \Exception - */ - public function createAPaymentTokenForTheCustomer(int $customer_id = 1, $gateway_id = 'ppcp-gateway'): WC_Payment_Token_CC - { - $this->createCustomerIfNotExists($customer_id); + /** + * Creates a payment token for a customer. + * + * @param int $customer_id The customer ID. + * @return WC_Payment_Token_CC The created payment token. + * @throws \Exception + */ + public function createAPaymentTokenForTheCustomer(int $customer_id = 1, $gateway_id = 'ppcp-gateway'): WC_Payment_Token_CC + { + $this->createCustomerIfNotExists($customer_id); - $token = new WC_Payment_Token_CC(); - $token->set_token('test_token_' . uniqid()); // Unique token ID - $token->set_gateway_id($gateway_id); - $token->set_user_id($customer_id); + $token = new WC_Payment_Token_CC(); + $token->set_token('test_token_' . uniqid()); // Unique token ID + $token->set_gateway_id($gateway_id); + $token->set_user_id($customer_id); - // These fields are required for WC_Payment_Token_CC - $token->set_card_type('visa'); // lowercase is often expected - $token->set_last4('1234'); - $token->set_expiry_month('12'); - $token->set_expiry_year('2030'); // Missing expiry year in your original code + // These fields are required for WC_Payment_Token_CC + $token->set_card_type('visa'); // lowercase is often expected + $token->set_last4('1234'); + $token->set_expiry_month('12'); + $token->set_expiry_year('2030'); // Missing expiry year in your original code - $result = $token->save(); + $result = $token->save(); - if (!$result || is_wp_error($result)) { - throw new \Exception('Failed to save payment token: ' . - (is_wp_error($result) ? $result->get_error_message() : 'Unknown error')); - } + if (!$result || is_wp_error($result)) { + throw new \Exception('Failed to save payment token: ' . + (is_wp_error($result) ? $result->get_error_message() : 'Unknown error')); + } - $saved_token = \WC_Payment_Tokens::get($token->get_id()); - if (!$saved_token || $saved_token->get_id() !== $token->get_id()) { - throw new \Exception('Token was not saved correctly'); - } + $saved_token = \WC_Payment_Tokens::get($token->get_id()); + if (!$saved_token || $saved_token->get_id() !== $token->get_id()) { + throw new \Exception('Token was not saved correctly'); + } - return $token; - } + return $token; + } - /** - * Helper method to create a subscription for testing. - * - * @param int $customer_id The customer ID - * @param string $payment_method The payment method - * @param string $sku - * @return WC_Subscription - * @throws \WC_Data_Exception - */ - public function createSubscription(int $customer_id = 1, string $payment_method = 'ppcp-gateway', $sku = 'DUMMY SUB SKU'): WC_Subscription - { - $product_id = wc_get_product_id_by_sku($sku); + /** + * Helper method to create a subscription for testing. + * + * @param int $customer_id The customer ID + * @param string $payment_method The payment method + * @param string $sku + * @return WC_Subscription + * @throws \WC_Data_Exception + */ + public function createSubscription(int $customer_id = 1, string $payment_method = 'ppcp-gateway', $sku = 'DUMMY SUB SKU'): WC_Subscription + { + $product_id = wc_get_product_id_by_sku($sku); $order = $this->getConfiguredOrder( $this->customer_id, $payment_method, ['subscription'] ); - $subscription = new WC_Subscription(); - $subscription->set_customer_id($customer_id); - $subscription->set_payment_method($payment_method); - $subscription->set_status('active'); - $subscription->set_parent_id($order->get_id()); - $subscription->set_billing_period('month'); - $subscription->set_billing_interval(1); + $subscription = new WC_Subscription(); + $subscription->set_customer_id($customer_id); + $subscription->set_payment_method($payment_method); + $subscription->set_status('active'); + $subscription->set_parent_id($order->get_id()); + $subscription->set_billing_period('month'); + $subscription->set_billing_interval(1); - // Add a product to the subscription - $subscription_item = new WC_Order_Item_Product(); - $subscription_item->set_props([ - 'product_id' => $product_id, - 'quantity' => 1, - 'subtotal' => 10, - 'total' => 10, - ]); - $subscription->add_item($subscription_item); - $subscription->set_date_created(current_time('mysql')); - $subscription->set_start_date(current_time('mysql')); - $subscription->set_next_payment_date(date('Y-m-d H:i:s', strtotime('+1 month', current_time('timestamp')))); - $subscription->save(); + // Add a product to the subscription + $subscription_item = new WC_Order_Item_Product(); + $subscription_item->set_props([ + 'product_id' => $product_id, + 'quantity' => 1, + 'subtotal' => 10, + 'total' => 10, + ]); + $subscription->add_item($subscription_item); + $subscription->set_date_created(current_time('mysql')); + $subscription->set_start_date(current_time('mysql')); + $subscription->set_next_payment_date(date('Y-m-d H:i:s', strtotime('+1 month', current_time('timestamp')))); + $subscription->save(); - return $subscription; - } + return $subscription; + } - /** - * Creates a renewal order for testing - * - * @param int $customer_id - * @param string $gateway_id - * @param int $subscription_id - * @return WC_Order - */ - protected function createRenewalOrder(int $customer_id, string $gateway_id, int $subscription_id): WC_Order - { + /** + * Creates a renewal order for testing + * + * @param int $customer_id + * @param string $gateway_id + * @param int $subscription_id + * @return WC_Order + */ + protected function createRenewalOrder(int $customer_id, string $gateway_id, int $subscription_id): WC_Order + { $renewal_order = $this->getConfiguredOrder( $customer_id, $gateway_id, @@ -195,81 +195,81 @@ class IntegrationMockedTestCase extends TestCase false ); $renewal_order->update_meta_data('_subscription_renewal', $subscription_id); - $renewal_order->update_meta_data('_subscription_renewal', $subscription_id); - $renewal_order->save(); + $renewal_order->update_meta_data('_subscription_renewal', $subscription_id); + $renewal_order->save(); - return $renewal_order; - } + return $renewal_order; + } - /** - * Mocks the OrderEndpoint to return a successful/failed order. - * - * @param string $intent The order intent (CAPTURE or AUTHORIZE) - * @param bool $success Whether the order was successful - * @return object The mocked OrderEndpoint - */ - public function mockOrderEndpoint(string $intent = 'CAPTURE', bool $order_success = true, bool $capture_success = true): object - { - $order_endpoint = \Mockery::mock(OrderEndpoint::class); - $order = \Mockery::mock(Order::class)->shouldIgnoreMissing(); + /** + * Mocks the OrderEndpoint to return a successful/failed order. + * + * @param string $intent The order intent (CAPTURE or AUTHORIZE) + * @param bool $success Whether the order was successful + * @return object The mocked OrderEndpoint + */ + public function mockOrderEndpoint(string $intent = 'CAPTURE', bool $order_success = true, bool $capture_success = true): object + { + $order_endpoint = \Mockery::mock(OrderEndpoint::class); + $order = \Mockery::mock(Order::class)->shouldIgnoreMissing(); - $order->shouldReceive('id')->andReturn('TEST-ORDER-' . uniqid()); - $order->shouldReceive('intent')->andReturn($intent); + $order->shouldReceive('id')->andReturn('TEST-ORDER-' . uniqid()); + $order->shouldReceive('intent')->andReturn($intent); - $order_status = \Mockery::mock(OrderStatus::class); - $order_status->shouldReceive('is')->andReturn($order_success); - $order_status->shouldReceive('name')->andReturn($order_success ? 'COMPLETED' : 'FAILED'); - $order->shouldReceive('status')->andReturn($order_status); + $order_status = \Mockery::mock(OrderStatus::class); + $order_status->shouldReceive('is')->andReturn($order_success); + $order_status->shouldReceive('name')->andReturn($order_success ? 'COMPLETED' : 'FAILED'); + $order->shouldReceive('status')->andReturn($order_status); $card_properties = new \stdClass(); $card_properties->brand = 'VISA'; $card_properties->last_digits = '1234'; $card_properties->expiry = '2026-12'; - $payment_source = \Mockery::mock(PaymentSource::class); - $payment_source->shouldReceive('name')->andReturn('card'); + $payment_source = \Mockery::mock(PaymentSource::class); + $payment_source->shouldReceive('name')->andReturn('card'); $payment_source->shouldReceive('properties')->andReturn($card_properties); - $order->shouldReceive('payment_source')->andReturn($payment_source); + $order->shouldReceive('payment_source')->andReturn($payment_source); - $purchase_unit = \Mockery::mock(PurchaseUnit::class)->shouldIgnoreMissing(); - $payments = \Mockery::mock(Payments::class)->shouldIgnoreMissing(); - $capture = \Mockery::mock(Capture::class)->shouldIgnoreMissing(); + $purchase_unit = \Mockery::mock(PurchaseUnit::class)->shouldIgnoreMissing(); + $payments = \Mockery::mock(Payments::class)->shouldIgnoreMissing(); + $capture = \Mockery::mock(Capture::class)->shouldIgnoreMissing(); - $capture->shouldReceive('id')->andReturn('TEST-CAPTURE-' . uniqid()); - $capture_status = \Mockery::mock(CaptureStatus::class)->shouldIgnoreMissing(); + $capture->shouldReceive('id')->andReturn('TEST-CAPTURE-' . uniqid()); + $capture_status = \Mockery::mock(CaptureStatus::class)->shouldIgnoreMissing(); - $capture_status->shouldReceive('name')->andReturn($capture_success ? 'COMPLETED' : 'DECLINED'); + $capture_status->shouldReceive('name')->andReturn($capture_success ? 'COMPLETED' : 'DECLINED'); $capture_status->shouldReceive('details')->andReturn(null); - $capture->shouldReceive('status')->andReturn($capture_status); + $capture->shouldReceive('status')->andReturn($capture_status); - // Mock authorizations for AUTHORIZE intent - if ($intent === 'AUTHORIZE') { - $authorization = \Mockery::mock(\WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization::class)->shouldIgnoreMissing(); + // Mock authorizations for AUTHORIZE intent + if ($intent === 'AUTHORIZE') { + $authorization = \Mockery::mock(\WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization::class)->shouldIgnoreMissing(); - $authorization->shouldReceive('id')->andReturn('TEST-AUTH-' . uniqid()); - $auth_status = \Mockery::mock(\WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus::class)->shouldIgnoreMissing(); + $authorization->shouldReceive('id')->andReturn('TEST-AUTH-' . uniqid()); + $auth_status = \Mockery::mock(\WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus::class)->shouldIgnoreMissing(); - $auth_status->shouldReceive('name')->andReturn($capture_success ? 'CREATED' : 'DENIED'); - $auth_status->shouldReceive('is')->andReturn($capture_success); - $authorization->shouldReceive('status')->andReturn($auth_status); - $payments->shouldReceive('authorizations')->andReturn([$authorization]); - $payments->shouldReceive('captures')->andReturn([]); - } else { - // For CAPTURE intent, set up captures but no authorizations - $payments->shouldReceive('captures')->andReturn([$capture]); - $payments->shouldReceive('authorizations')->andReturn([]); - } + $auth_status->shouldReceive('name')->andReturn($capture_success ? 'CREATED' : 'DENIED'); + $auth_status->shouldReceive('is')->andReturn($capture_success); + $authorization->shouldReceive('status')->andReturn($auth_status); + $payments->shouldReceive('authorizations')->andReturn([$authorization]); + $payments->shouldReceive('captures')->andReturn([]); + } else { + // For CAPTURE intent, set up captures but no authorizations + $payments->shouldReceive('captures')->andReturn([$capture]); + $payments->shouldReceive('authorizations')->andReturn([]); + } - $purchase_unit->shouldReceive('payments')->andReturn($payments); - $order->shouldReceive('purchase_units')->andReturn([$purchase_unit]); + $purchase_unit->shouldReceive('payments')->andReturn($payments); + $order->shouldReceive('purchase_units')->andReturn([$purchase_unit]); - // Set up the order endpoint methods - $order_endpoint->shouldReceive('create')->andReturn($order); - if ($intent === 'AUTHORIZE') { - $order_endpoint->shouldReceive('authorize')->andReturn($order); - } else { - $order_endpoint->shouldReceive('capture')->andReturn($order); - } - $order_endpoint->shouldReceive('order')->andReturn($order); + // Set up the order endpoint methods + $order_endpoint->shouldReceive('create')->andReturn($order); + if ($intent === 'AUTHORIZE') { + $order_endpoint->shouldReceive('authorize')->andReturn($order); + } else { + $order_endpoint->shouldReceive('capture')->andReturn($order); + } + $order_endpoint->shouldReceive('order')->andReturn($order); $order_endpoint->shouldReceive('patch_order_with')->andReturn($order); - return $order_endpoint; - } + return $order_endpoint; + } } diff --git a/tests/integration/PHPUnit/Traits/CleansTestData.php b/tests/integration/PHPUnit/Traits/CleansTestData.php index a07c972d1..8f0d911c8 100644 --- a/tests/integration/PHPUnit/Traits/CleansTestData.php +++ b/tests/integration/PHPUnit/Traits/CleansTestData.php @@ -5,21 +5,21 @@ namespace WooCommerce\PayPalCommerce\Tests\Integration\Traits; trait CleansTestData { - /** - * Clean all test data created by factories - */ - protected function cleanupTestData(): void - { - if (isset($this->order_factory)) { - $this->order_factory->cleanup(); - } + /** + * Clean all test data created by factories + */ + protected function cleanupTestData(): void + { + if (isset($this->order_factory)) { + $this->order_factory->cleanup(); + } - if (isset($this->product_factory)) { - $this->product_factory->cleanup(); - } + if (isset($this->product_factory)) { + $this->product_factory->cleanup(); + } - if (isset($this->coupon_factory)) { - $this->coupon_factory->cleanup(); - } - } + if (isset($this->coupon_factory)) { + $this->coupon_factory->cleanup(); + } + } } diff --git a/tests/integration/PHPUnit/Traits/CreateTestOrders.php b/tests/integration/PHPUnit/Traits/CreateTestOrders.php index 029465ef2..273f90977 100644 --- a/tests/integration/PHPUnit/Traits/CreateTestOrders.php +++ b/tests/integration/PHPUnit/Traits/CreateTestOrders.php @@ -8,42 +8,43 @@ use WooCommerce\PayPalCommerce\Tests\Integration\Factories\OrderFactory; trait CreateTestOrders { - use CreateTestProducts; + use CreateTestProducts; - private OrderFactory $order_factory; + private OrderFactory $order_factory; - /** - * Initialize order factory - */ - protected function initializeOrderFactory(): void - { - if (!isset($this->product_factory)) { - $this->initializeFactories(); - } + /** + * Initialize order factory + */ + protected function initializeOrderFactory(): void + { + if (!isset($this->product_factory)) { + $this->initializeFactories(); + } - $this->order_factory = new OrderFactory($this->product_factory, $this->coupon_factory); - } + $this->order_factory = new OrderFactory($this->product_factory, $this->coupon_factory); + } - /** - * Create a configured order using presets - */ - protected function getConfiguredOrder( - int $customer_id, - string $payment_method, - array $product_presets, - array $discount_presets = [], - bool $set_paid = true - ): WC_Order { - if (!isset($this->order_factory)) { - $this->initializeOrderFactory(); - } + /** + * Create a configured order using presets + */ + protected function getConfiguredOrder( + int $customer_id, + string $payment_method, + array $product_presets, + array $discount_presets = [], + bool $set_paid = true + ): WC_Order + { + if (!isset($this->order_factory)) { + $this->initializeOrderFactory(); + } - return $this->order_factory->create( - $customer_id, - $payment_method, - $product_presets, - $discount_presets, - $set_paid - ); - } + return $this->order_factory->create( + $customer_id, + $payment_method, + $product_presets, + $discount_presets, + $set_paid + ); + } } diff --git a/tests/integration/PHPUnit/Traits/CreateTestProducts.php b/tests/integration/PHPUnit/Traits/CreateTestProducts.php index 0584e4d4d..4020b8289 100644 --- a/tests/integration/PHPUnit/Traits/CreateTestProducts.php +++ b/tests/integration/PHPUnit/Traits/CreateTestProducts.php @@ -10,51 +10,51 @@ use WooCommerce\PayPalCommerce\Tests\Integration\Fixtures\DiscountPresets; trait CreateTestProducts { - private ProductFactory $product_factory; - private CouponFactory $coupon_factory; + private ProductFactory $product_factory; + private CouponFactory $coupon_factory; - /** - * Initialize factories - */ - protected function initializeFactories(): void - { - $this->product_factory = new ProductFactory(); - $this->coupon_factory = new CouponFactory(); - } + /** + * Initialize factories + */ + protected function initializeFactories(): void + { + $this->product_factory = new ProductFactory(); + $this->coupon_factory = new CouponFactory(); + } - /** - * Create all test products from presets - * @throws \WC_Data_Exception - */ - protected function createTestProducts(): void - { - if (!isset($this->product_factory)) { - $this->initializeFactories(); - } + /** + * Create all test products from presets + * @throws \WC_Data_Exception + */ + protected function createTestProducts(): void + { + if (!isset($this->product_factory)) { + $this->initializeFactories(); + } - foreach (array_keys(ProductPresets::get()) as $preset_name) { - $preset = ProductPresets::get()[$preset_name]; - // Only create if doesn't exist - if (!$this->product_factory->exists($preset['sku'])) { - $this->product_factory->createFromPreset($preset_name); - } - } - } + foreach (array_keys(ProductPresets::get()) as $preset_name) { + $preset = ProductPresets::get()[$preset_name]; + // Only create if doesn't exist + if (!$this->product_factory->exists($preset['sku'])) { + $this->product_factory->createFromPreset($preset_name); + } + } + } - /** - * Create all test coupons from presets - */ - protected function createTestCoupons(): void - { - if (!isset($this->coupon_factory)) { - $this->initializeFactories(); - } + /** + * Create all test coupons from presets + */ + protected function createTestCoupons(): void + { + if (!isset($this->coupon_factory)) { + $this->initializeFactories(); + } - foreach (DiscountPresets::get() as $preset_name => $preset) { - // Only create coupons (skip manual fees) - if (isset($preset['coupon_code']) && !$this->coupon_factory->exists($preset['coupon_code'])) { - $this->coupon_factory->createFromPreset($preset_name); - } - } - } + foreach (DiscountPresets::get() as $preset_name => $preset) { + // Only create coupons (skip manual fees) + if (isset($preset['coupon_code']) && !$this->coupon_factory->exists($preset['coupon_code'])) { + $this->coupon_factory->createFromPreset($preset_name); + } + } + } } diff --git a/tests/integration/PHPUnit/VaultingSubscriptionsTest.php b/tests/integration/PHPUnit/VaultingSubscriptionsTest.php index c61cb8253..bb994700e 100644 --- a/tests/integration/PHPUnit/VaultingSubscriptionsTest.php +++ b/tests/integration/PHPUnit/VaultingSubscriptionsTest.php @@ -21,73 +21,73 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; */ class VaultingSubscriptionsTest extends IntegrationMockedTestCase { - public function setUp(): void - { - parent::setUp(); + public function setUp(): void + { + parent::setUp(); $this->mockPaymentTokensEndpoint = \Mockery::mock(PaymentTokensEndpoint::class); } - /** - * Sets up a test container with common mocks - * - * @param OrderEndpoint $orderEndpoint - * @param array $additionalServices Additional services to override - * @return ContainerInterface - */ - protected function setupTestContainer(OrderEndpoint $orderEndpoint, array $additionalServices = []): ContainerInterface - { - $services = [ - 'api.endpoint.order' => function () use ($orderEndpoint) { - return $orderEndpoint; - }, - 'api.endpoint.payment-tokens' => function () { - return $this->mockPaymentTokensEndpoint; - } - ]; + /** + * Sets up a test container with common mocks + * + * @param OrderEndpoint $orderEndpoint + * @param array $additionalServices Additional services to override + * @return ContainerInterface + */ + protected function setupTestContainer(OrderEndpoint $orderEndpoint, array $additionalServices = []): ContainerInterface + { + $services = [ + 'api.endpoint.order' => function () use ($orderEndpoint) { + return $orderEndpoint; + }, + 'api.endpoint.payment-tokens' => function () { + return $this->mockPaymentTokensEndpoint; + } + ]; - return $this->bootstrapModule(array_merge($services, $additionalServices)); - } + return $this->bootstrapModule(array_merge($services, $additionalServices)); + } - /** - * Creates a payment token and configures the mock endpoint to return it - * - * @param int $customer_id - * @param string $gateway_id - * @return WC_Payment_Token - */ - protected function setupPaymentToken(int $customer_id, string $gateway_id = PayPalGateway::ID): WC_Payment_Token - { - $paymentToken = $this->createAPaymentTokenForTheCustomer($customer_id, $gateway_id); + /** + * Creates a payment token and configures the mock endpoint to return it + * + * @param int $customer_id + * @param string $gateway_id + * @return WC_Payment_Token + */ + protected function setupPaymentToken(int $customer_id, string $gateway_id = PayPalGateway::ID): WC_Payment_Token + { + $paymentToken = $this->createAPaymentTokenForTheCustomer($customer_id, $gateway_id); - $this->mockPaymentTokensEndpoint->shouldReceive('payment_tokens_for_customer') - ->andReturn([ - [ - 'id' => $paymentToken->get_token(), - 'payment_source' => new PaymentSource( - 'card', - (object)[ - 'last_digits' => $paymentToken->get_last4(), - 'brand' => $paymentToken->get_card_type(), - 'expiry' => $paymentToken->get_expiry_year() . '-' . $paymentToken->get_expiry_month() - ] - ) - ] - ]); + $this->mockPaymentTokensEndpoint->shouldReceive('payment_tokens_for_customer') + ->andReturn([ + [ + 'id' => $paymentToken->get_token(), + 'payment_source' => new PaymentSource( + 'card', + (object)[ + 'last_digits' => $paymentToken->get_last4(), + 'brand' => $paymentToken->get_card_type(), + 'expiry' => $paymentToken->get_expiry_year() . '-' . $paymentToken->get_expiry_month() + ] + ) + ] + ]); - return $paymentToken; - } + return $paymentToken; + } - /** - * Tests that vaulting is automatically enabled when subscription mode is set to vaulting_api. - * - * GIVEN a PayPal account with Reference Transactions enabled - * WHEN the subscription mode is set to "vaulting_api" - * THEN vaulting should be automatically enabled for the PayPal gateway - */ - public function test_vaulting_is_enabled_when_subscription_mode_is_vaulting_api() - { + /** + * Tests that vaulting is automatically enabled when subscription mode is set to vaulting_api. + * + * GIVEN a PayPal account with Reference Transactions enabled + * WHEN the subscription mode is set to "vaulting_api" + * THEN vaulting should be automatically enabled for the PayPal gateway + */ + public function test_vaulting_is_enabled_when_subscription_mode_is_vaulting_api() + { $user_has_cap_callback = function ($allcaps, $caps, $args) { if (isset($args[0]) && $args[0] === 'manage_woocommerce') { $allcaps['manage_woocommerce'] = true; @@ -159,100 +159,102 @@ class VaultingSubscriptionsTest extends IntegrationMockedTestCase remove_filter('user_has_cap', $user_has_cap_callback, 10); } - } - /** - * Data provider for payment gateway tests - */ - public function paymentGatewayProvider(): array - { - return [ - 'PayPal Gateway' => [PayPalGateway::ID], - 'Credit Card Gateway' => [CreditCardGateway::ID] - ]; - } + } - /** - * Tests PayPal renewal payment processing. - * - * GIVEN a subscription with a saved PayPal payment token due for renewal - * WHEN the renewal process is triggered - * THEN a new PayPal order should be created using the customer token - * - * @dataProvider paymentGatewayProvider - */ - public function test_renewal_payment_processing(string $gateway_id) - { - $mockOrderEndpoint = $this->mockOrderEndpoint(); - $c = $this->setupTestContainer($mockOrderEndpoint); + /** + * Data provider for payment gateway tests + */ + public function paymentGatewayProvider(): array + { + return [ + 'PayPal Gateway' => [PayPalGateway::ID], + 'Credit Card Gateway' => [CreditCardGateway::ID] + ]; + } + + /** + * Tests PayPal renewal payment processing. + * + * GIVEN a subscription with a saved PayPal payment token due for renewal + * WHEN the renewal process is triggered + * THEN a new PayPal order should be created using the customer token + * + * @dataProvider paymentGatewayProvider + */ + public function test_renewal_payment_processing(string $gateway_id) + { + $mockOrderEndpoint = $this->mockOrderEndpoint(); + $c = $this->setupTestContainer($mockOrderEndpoint); $this->setupPaymentToken($this->customer_id, $gateway_id); - $subscription = $this->createSubscription($this->customer_id, $gateway_id); - $renewal_order = $this->createRenewalOrder($this->customer_id, $gateway_id, $subscription->get_id()); + $subscription = $this->createSubscription($this->customer_id, $gateway_id); + $renewal_order = $this->createRenewalOrder($this->customer_id, $gateway_id, $subscription->get_id()); - $renewal_handler = $c->get('wc-subscriptions.renewal-handler'); - $renewal_handler->renew($renewal_order); + $renewal_handler = $c->get('wc-subscriptions.renewal-handler'); + $renewal_handler->renew($renewal_order); - // Check that the order was processed - $this->assertEquals('processing', $renewal_order->get_status(), 'The renewal order should be processing after successful payment'); - $this->assertNotEmpty($renewal_order->get_transaction_id(), 'The renewal order should have a transaction ID'); - } - /** - * Tests that renewal processing handles failed payments correctly. - * - * GIVEN a subscription due for renewal - * WHEN the payment process fails with an exception - * THEN the renewal order should be marked as failed - */ - public function test_renewal_handles_failed_payment() - { - $mockOrderEndpoint = $this->mockOrderEndpoint('CAPTURE', false, false); - $c = $this->setupTestContainer($mockOrderEndpoint); + // Check that the order was processed + $this->assertEquals('processing', $renewal_order->get_status(), 'The renewal order should be processing after successful payment'); + $this->assertNotEmpty($renewal_order->get_transaction_id(), 'The renewal order should have a transaction ID'); + } + + /** + * Tests that renewal processing handles failed payments correctly. + * + * GIVEN a subscription due for renewal + * WHEN the payment process fails with an exception + * THEN the renewal order should be marked as failed + */ + public function test_renewal_handles_failed_payment() + { + $mockOrderEndpoint = $this->mockOrderEndpoint('CAPTURE', false, false); + $c = $this->setupTestContainer($mockOrderEndpoint); $this->setupPaymentToken($this->customer_id); - $subscription = $this->createSubscription($this->customer_id, PayPalGateway::ID); - $renewal_order = $this->createRenewalOrder($this->customer_id, PayPalGateway::ID, $subscription->get_id()); - $renewal_handler = $c->get('wc-subscriptions.renewal-handler'); - $renewal_handler->renew($renewal_order); + $subscription = $this->createSubscription($this->customer_id, PayPalGateway::ID); + $renewal_order = $this->createRenewalOrder($this->customer_id, PayPalGateway::ID, $subscription->get_id()); + $renewal_handler = $c->get('wc-subscriptions.renewal-handler'); + $renewal_handler->renew($renewal_order); - // Check that the order status is failed - $this->assertEquals('failed', $renewal_order->get_status(), 'The renewal order should be marked as failed when payment fails'); - } + // Check that the order status is failed + $this->assertEquals('failed', $renewal_order->get_status(), 'The renewal order should be marked as failed when payment fails'); + } - /** - * Tests authorization-only subscription renewals. - * - * GIVEN the payment intent is set to "AUTHORIZE" - * WHEN a subscription renewal payment is processed - * THEN the payment should be authorized but not captured - */ - public function test_authorize_only_subscription_renewal() - { - // Mock the OrderEndpoint with AUTHORIZE intent - $mockOrderEndpoint = $this->mockOrderEndpoint('AUTHORIZE', false, true); - $c = $this->setupTestContainer($mockOrderEndpoint); + /** + * Tests authorization-only subscription renewals. + * + * GIVEN the payment intent is set to "AUTHORIZE" + * WHEN a subscription renewal payment is processed + * THEN the payment should be authorized but not captured + */ + public function test_authorize_only_subscription_renewal() + { + // Mock the OrderEndpoint with AUTHORIZE intent + $mockOrderEndpoint = $this->mockOrderEndpoint('AUTHORIZE', false, true); + $c = $this->setupTestContainer($mockOrderEndpoint); - // Setup payment token and subscription + // Setup payment token and subscription $this->setupPaymentToken($this->customer_id); - $subscription = $this->createSubscription($this->customer_id, PayPalGateway::ID); - $renewal_order = $this->createRenewalOrder($this->customer_id, PayPalGateway::ID, $subscription->get_id()); + $subscription = $this->createSubscription($this->customer_id, PayPalGateway::ID); + $renewal_order = $this->createRenewalOrder($this->customer_id, PayPalGateway::ID, $subscription->get_id()); - // Override the intent setting to ensure it's set to AUTHORIZE - $settings = $c->get('wcgateway.settings'); - $original_intent = $settings->get('intent'); - $settings->set('intent', 'authorize'); - $settings->persist(); + // Override the intent setting to ensure it's set to AUTHORIZE + $settings = $c->get('wcgateway.settings'); + $original_intent = $settings->get('intent'); + $settings->set('intent', 'authorize'); + $settings->persist(); - try { - // Process the renewal - $renewal_handler = $c->get('wc-subscriptions.renewal-handler'); - $renewal_handler->renew($renewal_order); + try { + // Process the renewal + $renewal_handler = $c->get('wc-subscriptions.renewal-handler'); + $renewal_handler->renew($renewal_order); - // Check that the order was processed with authorization - $this->assertEquals('on-hold', $renewal_order->get_status(), 'The renewal order should be on-hold after successful authorization'); - $this->assertNotEmpty($renewal_order->get_transaction_id(), 'The renewal order should have a transaction ID'); - $this->assertEquals('AUTHORIZE', $mockOrderEndpoint->order('')->intent(), 'The order intent should be AUTHORIZE'); - } finally { - // Restore original settings - $settings->set('intent', $original_intent); - $settings->persist(); - } - } + // Check that the order was processed with authorization + $this->assertEquals('on-hold', $renewal_order->get_status(), 'The renewal order should be on-hold after successful authorization'); + $this->assertNotEmpty($renewal_order->get_transaction_id(), 'The renewal order should have a transaction ID'); + $this->assertEquals('AUTHORIZE', $mockOrderEndpoint->order('')->intent(), 'The order intent should be AUTHORIZE'); + } finally { + // Restore original settings + $settings->set('intent', $original_intent); + $settings->persist(); + } + } }