From a0873f4f1c41f5a7b25b98555114b7171794386a Mon Sep 17 00:00:00 2001 From: Gus Ohnesorge Date: Thu, 2 Oct 2025 15:23:35 -0500 Subject: [PATCH 1/9] first pass at new service --- Model/Api.php | 43 +++++------ Model/ProductTicService.php | 139 ++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 26 deletions(-) create mode 100644 Model/ProductTicService.php diff --git a/Model/Api.php b/Model/Api.php index 588ab20..072cd3b 100644 --- a/Model/Api.php +++ b/Model/Api.php @@ -116,6 +116,13 @@ class Api */ private $cartItemResponseHandler; + /** + * Product TIC Service + * + * @var \Taxcloud\Magento2\Model\ProductTicService + */ + private $productTicService; + /** * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Framework\App\CacheInterface $cacheType @@ -125,6 +132,9 @@ class Api * @param \Magento\Catalog\Model\ProductFactory $productFactory * @param \Magento\Directory\Model\RegionFactory $regionFactory * @param \Taxcloud\Magento2\Logger\Logger $tclogger + * @param SerializerInterface $serializer + * @param \Taxcloud\Magento2\Model\CartItemResponseHandler $cartItemResponseHandler + * @param \Taxcloud\Magento2\Model\ProductTicService $productTicService * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -137,7 +147,8 @@ public function __construct( \Magento\Directory\Model\RegionFactory $regionFactory, \Taxcloud\Magento2\Logger\Logger $tclogger, SerializerInterface $serializer, - \Taxcloud\Magento2\Model\CartItemResponseHandler $cartItemResponseHandler + \Taxcloud\Magento2\Model\CartItemResponseHandler $cartItemResponseHandler, + \Taxcloud\Magento2\Model\ProductTicService $productTicService ) { $this->_scopeConfig = $scopeConfig; $this->_cacheType = $cacheType; @@ -148,6 +159,7 @@ public function __construct( $this->_regionFactory = $regionFactory; $this->serializer = $serializer; $this->cartItemResponseHandler = $cartItemResponseHandler; + $this->productTicService = $productTicService; if ($scopeConfig->getValue('tax/taxcloud_settings/logging', \Magento\Store\Model\ScopeInterface::SCOPE_STORE)) { $this->_tclogger = $tclogger; } else { @@ -186,23 +198,6 @@ protected function _getGuestCustomerId() return $this->_scopeConfig->getValue('tax/taxcloud_settings/guest_customer_id', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) ?? '-1'; } - /** - * Get TaxCloud Default Product TIC - * @return string - */ - protected function _getDefaultTic() - { - return $this->_scopeConfig->getValue('tax/taxcloud_settings/default_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) ?? '00000'; - } - - /** - * Get TaxCloud Default Shipping TIC - * @return string - */ - protected function _getShippingTic() - { - return $this->_scopeConfig->getValue('tax/taxcloud_settings/shipping_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) ?? '11010'; - } /** * Get TaxCloud Cache Lifetime @@ -331,16 +326,14 @@ public function lookupTaxes($itemsByType, $shippingAssignment, $quote) if (isset($itemsByType[self::ITEM_TYPE_PRODUCT])) { foreach ($itemsByType[self::ITEM_TYPE_PRODUCT] as $code => $itemTaxDetail) { $item = $keyedAddressItems[$code]; - if ($item->getProduct()->getTaxClassId() === '0') { + if ($item->getProduct() && $item->getProduct()->getTaxClassId() === '0') { // Skip products with tax_class_id of None, store owners should avoid doing this continue; } - $productModel = $this->_productFactory->create()->load($item->getProduct()->getId()); - $tic = $productModel->getCustomAttribute('taxcloud_tic'); $cartItems[] = array( 'ItemID' => $item->getSku(), 'Index' => $index, - 'TIC' => $tic ? $tic->getValue() : $this->_getDefaultTic(), + 'TIC' => $this->productTicService->getProductTic($item, 'lookupTaxes'), 'Price' => $item->getPrice() - $item->getDiscountAmount() / $item->getQty(), 'Qty' => $item->getQty(), ); @@ -354,7 +347,7 @@ public function lookupTaxes($itemsByType, $shippingAssignment, $quote) $cartItems[] = array( 'ItemID' => 'shipping', 'Index' => $index++, - 'TIC' => $this->_getShippingTic(), + 'TIC' => $this->productTicService->getShippingTic(), 'Price' => $itemTaxDetail[self::KEY_ITEM]->getRowTotal(), 'Qty' => 1, ); @@ -604,12 +597,10 @@ public function returnOrder($creditmemo) if ($items) { foreach ($items as $creditItem) { $item = $creditItem->getOrderItem(); - $productModel = $this->_productFactory->create()->load($item->getProduct()->getId()); - $tic = $productModel->getCustomAttribute('taxcloud_tic'); $cartItems[] = array( 'ItemID' => $item->getSku(), 'Index' => $index, - 'TIC' => $tic ? $tic->getValue() : $this->_getDefaultTic(), + 'TIC' => $this->productTicService->getProductTic($item, 'returnOrder'), 'Price' => $creditItem->getPrice() - $creditItem->getDiscountAmount() / $creditItem->getQty(), 'Qty' => $creditItem->getQty(), ); diff --git a/Model/ProductTicService.php b/Model/ProductTicService.php new file mode 100644 index 0000000..04f6434 --- /dev/null +++ b/Model/ProductTicService.php @@ -0,0 +1,139 @@ + + * @copyright 2021 The Federal Tax Authority, LLC d/b/a TaxCloud + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ + +namespace Taxcloud\Magento2\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Catalog\Model\ProductFactory; +use Taxcloud\Magento2\Logger\Logger; + +/** + * Service for handling Product TIC (Taxability Information Code) logic + * Handles cases where products have been deleted or don't have custom TIC attributes + */ +class ProductTicService +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var ProductFactory + */ + private $productFactory; + + /** + * @var Logger + */ + private $logger; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param ProductFactory $productFactory + * @param Logger $logger + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + ProductFactory $productFactory, + Logger $logger + ) { + $this->scopeConfig = $scopeConfig; + $this->productFactory = $productFactory; + $this->logger = $logger; + } + + /** + * Get product TIC (Taxability Information Code) with null safety + * Handles cases where product has been deleted or doesn't have custom TIC + * + * @param \Magento\Sales\Model\Order\Item $item + * @param string $context Context for logging (e.g., 'lookupTaxes', 'returnOrder') + * @return string The TIC value + */ + public function getProductTic($item, $context = '') + { + $product = $item->getProduct(); + + // Handle case where product has been deleted + if (!$product || !$product->getId()) { + $this->logger->info('Product not found for item ' . $item->getSku() . ' in ' . $context . ', using default TIC'); + return $this->getDefaultTic(); + } + + $productModel = $this->productFactory->create()->load($product->getId()); + $tic = $productModel->getCustomAttribute('taxcloud_tic'); + + return $tic ? $tic->getValue() : $this->getDefaultTic(); + } + + /** + * Get the default TIC value from configuration + * + * @return string + */ + public function getDefaultTic() + { + return $this->scopeConfig->getValue( + 'tax/taxcloud_settings/default_tic', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ) ?? '00000'; + } + + /** + * Check if a product exists and is valid + * + * @param \Magento\Sales\Model\Order\Item $item + * @return bool + */ + public function isProductValid($item) + { + $product = $item->getProduct(); + return $product && $product->getId(); + } + + /** + * Get product TIC with additional validation + * + * @param \Magento\Sales\Model\Order\Item $item + * @param string $context + * @return array Array with 'tic' (string) and 'isValid' (boolean) + */ + public function getProductTicWithValidation($item, $context = '') + { + $isValid = $this->isProductValid($item); + $tic = $this->getProductTic($item, $context); + + return [ + 'tic' => $tic, + 'isValid' => $isValid + ]; + } + + /** + * Get the shipping TIC value from configuration + * + * @return string + */ + public function getShippingTic() + { + return $this->scopeConfig->getValue( + 'tax/taxcloud_settings/shipping_tic', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ) ?? '11010'; + } +} From 6c7e911358df26e08850e48cd4b9a8f8cbafad9c Mon Sep 17 00:00:00 2001 From: Gus Ohnesorge Date: Thu, 2 Oct 2025 16:51:05 -0500 Subject: [PATCH 2/9] unused function --- Model/ProductTicService.php | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Model/ProductTicService.php b/Model/ProductTicService.php index 04f6434..5ecdd70 100644 --- a/Model/ProductTicService.php +++ b/Model/ProductTicService.php @@ -106,24 +106,6 @@ public function isProductValid($item) return $product && $product->getId(); } - /** - * Get product TIC with additional validation - * - * @param \Magento\Sales\Model\Order\Item $item - * @param string $context - * @return array Array with 'tic' (string) and 'isValid' (boolean) - */ - public function getProductTicWithValidation($item, $context = '') - { - $isValid = $this->isProductValid($item); - $tic = $this->getProductTic($item, $context); - - return [ - 'tic' => $tic, - 'isValid' => $isValid - ]; - } - /** * Get the shipping TIC value from configuration * From 75e69297c2742c5a5a20e2492034c6926f6a09b3 Mon Sep 17 00:00:00 2001 From: Gus Ohnesorge Date: Fri, 3 Oct 2025 11:01:14 -0500 Subject: [PATCH 3/9] linting --- Model/ProductTicService.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Model/ProductTicService.php b/Model/ProductTicService.php index 5ecdd70..381bc9e 100644 --- a/Model/ProductTicService.php +++ b/Model/ProductTicService.php @@ -60,7 +60,7 @@ public function __construct( /** * Get product TIC (Taxability Information Code) with null safety * Handles cases where product has been deleted or doesn't have custom TIC - * + * * @param \Magento\Sales\Model\Order\Item $item * @param string $context Context for logging (e.g., 'lookupTaxes', 'returnOrder') * @return string The TIC value @@ -71,7 +71,9 @@ public function getProductTic($item, $context = '') // Handle case where product has been deleted if (!$product || !$product->getId()) { - $this->logger->info('Product not found for item ' . $item->getSku() . ' in ' . $context . ', using default TIC'); + $this->logger->info( + 'Product not found for item ' . $item->getSku() . ' in ' . $context . ', using default TIC' + ); return $this->getDefaultTic(); } @@ -83,20 +85,20 @@ public function getProductTic($item, $context = '') /** * Get the default TIC value from configuration - * + * * @return string */ public function getDefaultTic() { return $this->scopeConfig->getValue( - 'tax/taxcloud_settings/default_tic', + 'tax/taxcloud_settings/default_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE ) ?? '00000'; } /** * Check if a product exists and is valid - * + * * @param \Magento\Sales\Model\Order\Item $item * @return bool */ @@ -108,13 +110,13 @@ public function isProductValid($item) /** * Get the shipping TIC value from configuration - * + * * @return string */ public function getShippingTic() { return $this->scopeConfig->getValue( - 'tax/taxcloud_settings/shipping_tic', + 'tax/taxcloud_settings/shipping_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE ) ?? '11010'; } From 4fd7c5385c5eaa7fbc9a99fb406c0075285f554a Mon Sep 17 00:00:00 2001 From: Gus Ohnesorge Date: Fri, 3 Oct 2025 13:13:37 -0500 Subject: [PATCH 4/9] create magento mocking to support unit tests that have magento framework dependency --- .github/actions/run-tests/action.yml | 7 +- .github/workflows/test.yml | 6 +- Makefile | 9 +- Test/Unit/Mocks/MagentoMocks.php | 250 ++++++++++++++++++ Test/Unit/Model/ProductTicServiceTest.php | 295 ++++++++++++++++++++++ Test/Unit/bootstrap.php | 11 + composer.json | 8 +- 7 files changed, 579 insertions(+), 7 deletions(-) create mode 100644 Test/Unit/Mocks/MagentoMocks.php create mode 100644 Test/Unit/Model/ProductTicServiceTest.php create mode 100644 Test/Unit/bootstrap.php diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index 0512c8f..e9b1ea8 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -23,7 +23,12 @@ runs: - name: Install dependencies shell: bash run: | - composer install --no-dev --optimize-autoloader + composer install --optimize-autoloader + + - name: Run unit tests + shell: bash + run: | + vendor/bin/phpunit Test/Unit/ --testdox - name: Run integration tests shell: bash diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 82a5fe7..0cb83f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,11 @@ jobs: - name: Install dependencies run: | - composer install --no-dev --optimize-autoloader + composer install --optimize-autoloader + + - name: Run Unit Tests + run: | + vendor/bin/phpunit Test/Unit/ --testdox - name: Run Integration Tests run: | diff --git a/Makefile b/Makefile index 463f094..0e055c5 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,11 @@ -.PHONY: test test-local test-compatibility lint lint-fix help +.PHONY: test test-local test-unit test-compatibility lint lint-fix help # Default target help: @echo "Available commands:" @echo " make test - Run all tests using Docker" @echo " make test-local - Run all tests locally (requires PHP)" + @echo " make test-unit - Run unit tests only" @echo " make test-compatibility - Run Adobe Commerce 2.4.8-p1 compatibility tests" @echo " make lint - Run PHP CodeSniffer linting" @echo " make lint-fix - Auto-fix linting issues where possible" @@ -18,11 +19,17 @@ test: # Run tests locally (requires PHP) test-local: @echo "Running all tests locally..." + @vendor/bin/phpunit Test/Unit/ --testdox @for test_file in Test/Integration/*.php; do \ echo "Running $$test_file..."; \ php "$$test_file"; \ done +# Run unit tests only +test-unit: + @echo "Running unit tests..." + @vendor/bin/phpunit Test/Unit/ --testdox + # Run Adobe Commerce 2.4.8-p1 compatibility tests test-compatibility: @echo "Running Adobe Commerce 2.4.8-p1 compatibility tests..." diff --git a/Test/Unit/Mocks/MagentoMocks.php b/Test/Unit/Mocks/MagentoMocks.php new file mode 100644 index 0000000..f43c553 --- /dev/null +++ b/Test/Unit/Mocks/MagentoMocks.php @@ -0,0 +1,250 @@ + + * @copyright 2021 The Federal Tax Authority, LLC d/b/a TaxCloud + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ + +namespace Taxcloud\Magento2\Test\Unit\Model; + +// Load Magento mocks before any other includes +require_once __DIR__ . '/../Mocks/MagentoMocks.php'; + +// Load the ProductTicService class directly +require_once __DIR__ . '/../../../Model/ProductTicService.php'; + +use PHPUnit\Framework\TestCase; +use Taxcloud\Magento2\Model\ProductTicService; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Model\Product; +use Magento\Sales\Model\Order\Item; +use Magento\Framework\Api\AttributeValue; +use Taxcloud\Magento2\Logger\Logger; + +class ProductTicServiceTest extends TestCase +{ + private $productTicService; + private $scopeConfig; + private $productFactory; + private $logger; + + protected function setUp(): void + { + $this->scopeConfig = $this->createMock(ScopeConfigInterface::class); + $this->productFactory = $this->createMock(ProductFactory::class); + $this->logger = $this->createMock(Logger::class); + + $this->productTicService = new ProductTicService( + $this->scopeConfig, + $this->productFactory, + $this->logger + ); + } + + /** + * Test getProductTic with valid product having custom TIC + */ + public function testGetProductTicWithValidProductAndCustomTic() + { + $item = $this->createMock(Item::class); + $product = $this->createMock(Product::class); + $productModel = $this->createMock(Product::class); + $customAttribute = $this->createMock(AttributeValue::class); + + $item->method('getSku')->willReturn('TEST_SKU'); + $item->method('getProduct')->willReturn($product); + + $product->method('getId')->willReturn(123); + + $productModel->method('load')->with(123)->willReturnSelf(); + $productModel->method('getCustomAttribute')->with('taxcloud_tic')->willReturn($customAttribute); + + $customAttribute->method('getValue')->willReturn('20000'); + + $this->productFactory->method('create')->willReturn($productModel); + + $result = $this->productTicService->getProductTic($item, 'testContext'); + + $this->assertEquals('20000', $result, 'Should return custom TIC value when product has custom TIC'); + } + + /** + * Test getProductTic with deleted/null product + */ + public function testGetProductTicWithDeletedProduct() + { + $item = $this->createMock(Item::class); + $item->method('getSku')->willReturn('DELETED_SKU'); + $item->method('getProduct')->willReturn(null); + + $this->scopeConfig->method('getValue') + ->with('tax/taxcloud_settings/default_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) + ->willReturn('00000'); + + $this->logger->expects($this->once()) + ->method('info') + ->with('Product not found for item DELETED_SKU in testContext, using default TIC'); + + $result = $this->productTicService->getProductTic($item, 'testContext'); + + $this->assertEquals('00000', $result, 'Should return default TIC when product is null'); + } + + /** + * Test getProductTic with product having no ID + */ + public function testGetProductTicWithProductHavingNoId() + { + $item = $this->createMock(Item::class); + $product = $this->createMock(Product::class); + + $item->method('getSku')->willReturn('NO_ID_SKU'); + $item->method('getProduct')->willReturn($product); + $product->method('getId')->willReturn(null); + + $this->scopeConfig->method('getValue') + ->with('tax/taxcloud_settings/default_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) + ->willReturn('00000'); + + $this->logger->expects($this->once()) + ->method('info') + ->with('Product not found for item NO_ID_SKU in testContext, using default TIC'); + + $result = $this->productTicService->getProductTic($item, 'testContext'); + + $this->assertEquals('00000', $result, 'Should return default TIC when product has no ID'); + } + + /** + * Test getProductTic with product having no custom TIC attribute + */ + public function testGetProductTicWithNoCustomTicAttribute() + { + $item = $this->createMock(Item::class); + $product = $this->createMock(Product::class); + $productModel = $this->createMock(Product::class); + + $item->method('getSku')->willReturn('NO_TIC_SKU'); + $item->method('getProduct')->willReturn($product); + + $product->method('getId')->willReturn(456); + + $productModel->method('load')->with(456)->willReturnSelf(); + $productModel->method('getCustomAttribute')->with('taxcloud_tic')->willReturn(null); + + $this->productFactory->method('create')->willReturn($productModel); + + $this->scopeConfig->method('getValue') + ->with('tax/taxcloud_settings/default_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) + ->willReturn('00000'); + + $result = $this->productTicService->getProductTic($item, 'testContext'); + + $this->assertEquals('00000', $result, 'Should return default TIC when product has no custom TIC attribute'); + } + + /** + * Test getProductTic with empty context parameter + */ + public function testGetProductTicWithEmptyContext() + { + $item = $this->createMock(Item::class); + $item->method('getSku')->willReturn('TEST_SKU'); + $item->method('getProduct')->willReturn(null); + + $this->scopeConfig->method('getValue') + ->with('tax/taxcloud_settings/default_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) + ->willReturn('00000'); + + $this->logger->expects($this->once()) + ->method('info') + ->with('Product not found for item TEST_SKU in , using default TIC'); + + $result = $this->productTicService->getProductTic($item, ''); + + $this->assertEquals('00000', $result, 'Should return default TIC when context is empty'); + } + + /** + * Test getDefaultTic with configured value + */ + public function testGetDefaultTicWithConfiguredValue() + { + $this->scopeConfig->method('getValue') + ->with('tax/taxcloud_settings/default_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) + ->willReturn('12345'); + + // Execute the method + $result = $this->productTicService->getDefaultTic(); + + $this->assertEquals('12345', $result, 'Should return configured default TIC value'); + } + + /** + * Test getDefaultTic with null configuration (fallback) + */ + public function testGetDefaultTicWithNullConfiguration() + { + $this->scopeConfig->method('getValue') + ->with('tax/taxcloud_settings/default_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) + ->willReturn(null); + + $result = $this->productTicService->getDefaultTic(); + + $this->assertEquals('00000', $result, 'Should return fallback default TIC value when configuration is null'); + } + + /** + * Test isProductValid with valid product + */ + public function testIsProductValidWithValidProduct() + { + $item = $this->createMock(Item::class); + $product = $this->createMock(Product::class); + + $item->method('getProduct')->willReturn($product); + $product->method('getId')->willReturn(789); + + $result = $this->productTicService->isProductValid($item); + + $this->assertTrue($result, 'Should return true for valid product with ID'); + } + + /** + * Test isProductValid with null product + */ + public function testIsProductValidWithNullProduct() + { + $item = $this->createMock(Item::class); + $item->method('getProduct')->willReturn(null); + + $result = $this->productTicService->isProductValid($item); + + $this->assertFalse($result, 'Should return false for null product'); + } + + /** + * Test isProductValid with product having no ID + */ + public function testIsProductValidWithProductHavingNoId() + { + $item = $this->createMock(Item::class); + $product = $this->createMock(Product::class); + + $item->method('getProduct')->willReturn($product); + $product->method('getId')->willReturn(null); + + $result = $this->productTicService->isProductValid($item); + + $this->assertFalse($result, 'Should return false for product with no ID'); + } + + /** + * Test getShippingTic with configured value + */ + public function testGetShippingTicWithConfiguredValue() + { + $this->scopeConfig->method('getValue') + ->with('tax/taxcloud_settings/shipping_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) + ->willReturn('11010'); + + $result = $this->productTicService->getShippingTic(); + + $this->assertEquals('11010', $result, 'Should return configured shipping TIC value'); + } + + /** + * Test getShippingTic with null configuration (fallback) + */ + public function testGetShippingTicWithNullConfiguration() + { + $this->scopeConfig->method('getValue') + ->with('tax/taxcloud_settings/shipping_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) + ->willReturn(null); + + $result = $this->productTicService->getShippingTic(); + + $this->assertEquals('11010', $result, 'Should return fallback shipping TIC value when configuration is null'); + } + + /** + * Test getShippingTic with custom configured value + */ + public function testGetShippingTicWithCustomConfiguredValue() + { + $this->scopeConfig->method('getValue') + ->with('tax/taxcloud_settings/shipping_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) + ->willReturn('99999'); + + $result = $this->productTicService->getShippingTic(); + + $this->assertEquals('99999', $result, 'Should return custom configured shipping TIC value'); + } + +} diff --git a/Test/Unit/bootstrap.php b/Test/Unit/bootstrap.php new file mode 100644 index 0000000..e7129ef --- /dev/null +++ b/Test/Unit/bootstrap.php @@ -0,0 +1,11 @@ + Date: Fri, 3 Oct 2025 13:18:48 -0500 Subject: [PATCH 5/9] more data driven testing --- Model/ProductTicService.php | 12 ++- Test/Unit/Model/ProductTicServiceTest.php | 119 ++++++++-------------- 2 files changed, 50 insertions(+), 81 deletions(-) diff --git a/Model/ProductTicService.php b/Model/ProductTicService.php index 381bc9e..8e6ec31 100644 --- a/Model/ProductTicService.php +++ b/Model/ProductTicService.php @@ -90,10 +90,12 @@ public function getProductTic($item, $context = '') */ public function getDefaultTic() { - return $this->scopeConfig->getValue( + $value = $this->scopeConfig->getValue( 'tax/taxcloud_settings/default_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) ?? '00000'; + ); + + return ($value !== null && $value !== '') ? $value : '00000'; } /** @@ -115,9 +117,11 @@ public function isProductValid($item) */ public function getShippingTic() { - return $this->scopeConfig->getValue( + $value = $this->scopeConfig->getValue( 'tax/taxcloud_settings/shipping_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) ?? '11010'; + ); + + return ($value !== null && $value !== '') ? $value : '11010'; } } diff --git a/Test/Unit/Model/ProductTicServiceTest.php b/Test/Unit/Model/ProductTicServiceTest.php index de45ac5..c3397af 100644 --- a/Test/Unit/Model/ProductTicServiceTest.php +++ b/Test/Unit/Model/ProductTicServiceTest.php @@ -177,119 +177,84 @@ public function testGetProductTicWithEmptyContext() } /** - * Test getDefaultTic with configured value + * Test getDefaultTic with various configurations + * @dataProvider defaultTicProvider */ - public function testGetDefaultTicWithConfiguredValue() + public function testGetDefaultTic($configValue, $expectedResult, $description) { $this->scopeConfig->method('getValue') ->with('tax/taxcloud_settings/default_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) - ->willReturn('12345'); + ->willReturn($configValue); - // Execute the method $result = $this->productTicService->getDefaultTic(); - $this->assertEquals('12345', $result, 'Should return configured default TIC value'); + $this->assertEquals($expectedResult, $result, $description); } - /** - * Test getDefaultTic with null configuration (fallback) - */ - public function testGetDefaultTicWithNullConfiguration() - { - $this->scopeConfig->method('getValue') - ->with('tax/taxcloud_settings/default_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) - ->willReturn(null); - - $result = $this->productTicService->getDefaultTic(); - - $this->assertEquals('00000', $result, 'Should return fallback default TIC value when configuration is null'); - } - - /** - * Test isProductValid with valid product - */ - public function testIsProductValidWithValidProduct() - { - $item = $this->createMock(Item::class); - $product = $this->createMock(Product::class); - - $item->method('getProduct')->willReturn($product); - $product->method('getId')->willReturn(789); - - $result = $this->productTicService->isProductValid($item); - - $this->assertTrue($result, 'Should return true for valid product with ID'); - } - - /** - * Test isProductValid with null product - */ - public function testIsProductValidWithNullProduct() + public function defaultTicProvider() { - $item = $this->createMock(Item::class); - $item->method('getProduct')->willReturn(null); - - $result = $this->productTicService->isProductValid($item); - - $this->assertFalse($result, 'Should return false for null product'); + return [ + 'configured value' => ['12345', '12345', 'Should return configured default TIC value'], + 'null configuration' => [null, '00000', 'Should return fallback default TIC value when configuration is null'], + 'empty string' => ['', '00000', 'Should return fallback default TIC value when configuration is empty'], + 'zero value' => ['0', '0', 'Should return zero if configured as zero'], + ]; } /** - * Test isProductValid with product having no ID + * Test isProductValid with various product states + * @dataProvider productValidationProvider */ - public function testIsProductValidWithProductHavingNoId() + public function testIsProductValid($productId, $expectedResult, $description) { $item = $this->createMock(Item::class); - $product = $this->createMock(Product::class); - $item->method('getProduct')->willReturn($product); - $product->method('getId')->willReturn(null); + if ($productId === 'null_product') { + $item->method('getProduct')->willReturn(null); + } else { + $product = $this->createMock(Product::class); + $item->method('getProduct')->willReturn($product); + $product->method('getId')->willReturn($productId); + } $result = $this->productTicService->isProductValid($item); - $this->assertFalse($result, 'Should return false for product with no ID'); + $this->assertEquals($expectedResult, $result, $description); } - /** - * Test getShippingTic with configured value - */ - public function testGetShippingTicWithConfiguredValue() + public function productValidationProvider() { - $this->scopeConfig->method('getValue') - ->with('tax/taxcloud_settings/shipping_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) - ->willReturn('11010'); - - $result = $this->productTicService->getShippingTic(); - - $this->assertEquals('11010', $result, 'Should return configured shipping TIC value'); + return [ + 'valid product with ID' => [789, true, 'Should return true for valid product with ID'], + 'null product' => ['null_product', false, 'Should return false for null product'], + 'product with no ID' => [null, false, 'Should return false for product with no ID'], + 'product with zero ID' => [0, false, 'Should return false for product with zero ID'], + ]; } /** - * Test getShippingTic with null configuration (fallback) + * Test getShippingTic with various configurations + * @dataProvider shippingTicProvider */ - public function testGetShippingTicWithNullConfiguration() + public function testGetShippingTic($configValue, $expectedResult, $description) { $this->scopeConfig->method('getValue') ->with('tax/taxcloud_settings/shipping_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) - ->willReturn(null); + ->willReturn($configValue); $result = $this->productTicService->getShippingTic(); - $this->assertEquals('11010', $result, 'Should return fallback shipping TIC value when configuration is null'); + $this->assertEquals($expectedResult, $result, $description); } - /** - * Test getShippingTic with custom configured value - */ - public function testGetShippingTicWithCustomConfiguredValue() + public function shippingTicProvider() { - $this->scopeConfig->method('getValue') - ->with('tax/taxcloud_settings/shipping_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) - ->willReturn('99999'); - - $result = $this->productTicService->getShippingTic(); - - $this->assertEquals('99999', $result, 'Should return custom configured shipping TIC value'); + return [ + 'default configured value' => ['11010', '11010', 'Should return configured shipping TIC value'], + 'null configuration' => [null, '11010', 'Should return fallback shipping TIC value when configuration is null'], + 'custom configured value' => ['99999', '99999', 'Should return custom configured shipping TIC value'], + 'empty string' => ['', '11010', 'Should return fallback shipping TIC value when configuration is empty'], + ]; } } From b3d699701b2787a302f0f7891f1e49521bbff28e Mon Sep 17 00:00:00 2001 From: Gus Ohnesorge Date: Fri, 3 Oct 2025 13:22:15 -0500 Subject: [PATCH 6/9] updateapi tests --- Model/Api.php | 10 +++++++--- Test/Unit/Model/ApiTest.php | 28 +++++++++++++++++++++------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/Model/Api.php b/Model/Api.php index 072cd3b..ba4f798 100644 --- a/Model/Api.php +++ b/Model/Api.php @@ -614,7 +614,7 @@ public function returnOrder($creditmemo) $cartItems[] = array( 'ItemID' => 'shipping', 'Index' => $index, - 'TIC' => $this->_getShippingTic(), + 'TIC' => $this->productTicService->getShippingTic(), 'Price' => $shippingAmount, 'Qty' => 1, ); @@ -698,8 +698,12 @@ public function returnOrder($creditmemo) $returnResult = $obj->getResult(); - if ($returnResult['ResponseType'] != 'OK') { - $this->_tclogger->info('Error encountered during returnOrder: ' . $returnResult['Messages']['ResponseMessage']['Message']); + if (!$returnResult || $returnResult['ResponseType'] != 'OK') { + $errorMessage = 'Unknown error'; + if ($returnResult && isset($returnResult['Messages']['ResponseMessage']['Message'])) { + $errorMessage = $returnResult['Messages']['ResponseMessage']['Message']; + } + $this->_tclogger->info('Error encountered during returnOrder: ' . $errorMessage); return false; } diff --git a/Test/Unit/Model/ApiTest.php b/Test/Unit/Model/ApiTest.php index bf387e9..d295b6d 100644 --- a/Test/Unit/Model/ApiTest.php +++ b/Test/Unit/Model/ApiTest.php @@ -29,6 +29,8 @@ use Taxcloud\Magento2\Logger\Logger; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\DataObject; +use Taxcloud\Magento2\Model\CartItemResponseHandler; +use Taxcloud\Magento2\Model\ProductTicService; class ApiTest extends TestCase { @@ -42,6 +44,8 @@ class ApiTest extends TestCase private $regionFactory; private $logger; private $serializer; + private $cartItemResponseHandler; + private $productTicService; private $mockSoapClient; private $mockDataObject; @@ -56,8 +60,16 @@ protected function setUp(): void $this->regionFactory = $this->createMock(RegionFactory::class); $this->logger = $this->createMock(Logger::class); $this->serializer = $this->createMock(SerializerInterface::class); - $this->mockSoapClient = $this->createMock(\SoapClient::class); - $this->mockDataObject = $this->createMock(DataObject::class); + $this->cartItemResponseHandler = $this->createMock(CartItemResponseHandler::class); + $this->productTicService = $this->createMock(ProductTicService::class); + $this->mockSoapClient = $this->getMockBuilder(\SoapClient::class) + ->disableOriginalConstructor() + ->addMethods(['Returned']) + ->getMock(); + $this->mockDataObject = $this->getMockBuilder(DataObject::class) + ->disableOriginalConstructor() + ->addMethods(['setParams', 'getParams', 'setResult', 'getResult']) + ->getMock(); $this->api = new Api( $this->scopeConfig, @@ -68,7 +80,9 @@ protected function setUp(): void $this->productFactory, $this->regionFactory, $this->logger, - $this->serializer + $this->serializer, + $this->cartItemResponseHandler, + $this->productTicService ); } @@ -95,7 +109,7 @@ public function testReturnOrderIncludesReturnCoDeliveryFeeWhenNoCartItems() // Mock credit memo $creditmemo = $this->createMock(\Magento\Sales\Model\Order\Creditmemo::class); - $order = $this->createMock(\Magento\Sales\Model\Order::class); + $order = $this->createMock(\Magento\Sales\Model\Order\Order::class); $order->method('getIncrementId')->willReturn('TEST_ORDER_123'); $creditmemo->method('getOrder')->willReturn($order); @@ -157,7 +171,7 @@ public function testReturnOrderHandlesSoapErrorGracefully() // Mock credit memo $creditmemo = $this->createMock(\Magento\Sales\Model\Order\Creditmemo::class); - $order = $this->createMock(\Magento\Sales\Model\Order::class); + $order = $this->createMock(\Magento\Sales\Model\Order\Order::class); $order->method('getIncrementId')->willReturn('TEST_ORDER_123'); $creditmemo->method('getOrder')->willReturn($order); @@ -209,7 +223,7 @@ public function testReturnOrderWithCartItems() // Mock credit memo with items $creditmemo = $this->createMock(\Magento\Sales\Model\Order\Creditmemo::class); - $order = $this->createMock(\Magento\Sales\Model\Order::class); + $order = $this->createMock(\Magento\Sales\Model\Order\Order::class); $order->method('getIncrementId')->willReturn('TEST_ORDER_123'); $creditItem = $this->createMock(\Magento\Sales\Model\Order\Creditmemo\Item::class); @@ -312,7 +326,7 @@ public function testReturnOrderFailsWhenParameterIsLost() // Mock credit memo $creditmemo = $this->createMock(\Magento\Sales\Model\Order\Creditmemo::class); - $order = $this->createMock(\Magento\Sales\Model\Order::class); + $order = $this->createMock(\Magento\Sales\Model\Order\Order::class); $order->method('getIncrementId')->willReturn('TEST_ORDER_123'); $creditmemo->method('getOrder')->willReturn($order); From 525eabd7a365b4e462bcfb792c753861b2373440 Mon Sep 17 00:00:00 2001 From: Gus Ohnesorge Date: Fri, 3 Oct 2025 14:08:52 -0500 Subject: [PATCH 7/9] note about generating mocks --- Test/Unit/Mocks/MagentoMocks.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Test/Unit/Mocks/MagentoMocks.php b/Test/Unit/Mocks/MagentoMocks.php index f43c553..9e5f8a6 100644 --- a/Test/Unit/Mocks/MagentoMocks.php +++ b/Test/Unit/Mocks/MagentoMocks.php @@ -4,6 +4,23 @@ * * This file provides minimal class definitions needed for PHPUnit mocking * without requiring a full Magento installation. + * + * HOW TO GENERATE THESE MOCKS: + * + * 1. When you get "Class not found" errors during unit testing: + * - Add the missing class to the appropriate namespace below + * - Include only the methods that are actually called in your tests + * - Use empty method bodies: public function methodName() {} + * + * 2. For interfaces, just declare them without implementation + * 3. For classes, add minimal method signatures that your tests need + * 4. Keep it minimal - only add what's necessary for tests to run + * + * Example: + * class MyClass { + * public function getValue() {} + * public function setValue($value) { return $this; } + * } */ namespace Magento\Framework\App\Config { From a2fac84bed9f1eb7c0037f8ddcc318c84911ae48 Mon Sep 17 00:00:00 2001 From: Gus Ohnesorge Date: Fri, 3 Oct 2025 14:09:31 -0500 Subject: [PATCH 8/9] extra note --- Test/Unit/Mocks/MagentoMocks.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Test/Unit/Mocks/MagentoMocks.php b/Test/Unit/Mocks/MagentoMocks.php index 9e5f8a6..a8ace2e 100644 --- a/Test/Unit/Mocks/MagentoMocks.php +++ b/Test/Unit/Mocks/MagentoMocks.php @@ -6,6 +6,7 @@ * without requiring a full Magento installation. * * HOW TO GENERATE THESE MOCKS: + * (Asking AI to generate the mocks for the classes that are not found works pretty well) * * 1. When you get "Class not found" errors during unit testing: * - Add the missing class to the appropriate namespace below From a79e830dee153973c2a964a2f8501173805dde39 Mon Sep 17 00:00:00 2001 From: Gus Ohnesorge Date: Fri, 3 Oct 2025 14:23:40 -0500 Subject: [PATCH 9/9] consts for default tics --- Model/ProductTicService.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Model/ProductTicService.php b/Model/ProductTicService.php index 8e6ec31..f1512d8 100644 --- a/Model/ProductTicService.php +++ b/Model/ProductTicService.php @@ -27,6 +27,15 @@ */ class ProductTicService { + /** + * Default TIC fallback value when configuration is empty or null + */ + const DEFAULT_TIC = '00000'; + + /** + * Default shipping TIC fallback value when configuration is empty or null + */ + const DEFAULT_SHIPPING_TIC = '11010'; /** * @var ScopeConfigInterface */ @@ -85,6 +94,7 @@ public function getProductTic($item, $context = '') /** * Get the default TIC value from configuration + * Falls back to DEFAULT_TIC if configuration is empty or null * * @return string */ @@ -95,7 +105,7 @@ public function getDefaultTic() \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); - return ($value !== null && $value !== '') ? $value : '00000'; + return ($value !== null && $value !== '') ? $value : self::DEFAULT_TIC; } /** @@ -112,6 +122,7 @@ public function isProductValid($item) /** * Get the shipping TIC value from configuration + * Falls back to DEFAULT_SHIPPING_TIC if configuration is empty or null * * @return string */ @@ -122,6 +133,6 @@ public function getShippingTic() \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); - return ($value !== null && $value !== '') ? $value : '11010'; + return ($value !== null && $value !== '') ? $value : self::DEFAULT_SHIPPING_TIC; } }