diff --git a/Model/Api.php b/Model/Api.php index ce1b57c..91bc8af 100644 --- a/Model/Api.php +++ b/Model/Api.php @@ -372,8 +372,15 @@ public function lookupTaxes($itemsByType, $shippingAssignment, $quote) // Skip products with tax_class_id of None, store owners should avoid doing this continue; } + + // Create unique ItemID by combining SKU with quote item ID to distinguish between + // separate line items (e.g., regular item vs promo item with same SKU) + // We use the quote item ID (not tax calculation ID) so it matches returnOrder + $quoteItemId = $item->getId(); + $itemId = $quoteItemId ? $item->getSku() . '-' . $quoteItemId : $item->getSku(); + $cartItems[] = array( - 'ItemID' => $item->getSku(), + 'ItemID' => $itemId, 'Index' => $index, 'TIC' => $this->productTicService->getProductTic($item, 'lookupTaxes'), 'Price' => $item->getPrice() - $item->getDiscountAmount() / $item->getQty(), @@ -796,8 +803,15 @@ public function returnOrder($creditmemo) if ($items) { foreach ($items as $creditItem) { $item = $creditItem->getOrderItem(); + + // Create unique ItemID by combining SKU with quote item ID to distinguish between + // separate line items (e.g., regular item vs promo item with same SKU) + // We use quote item ID (from order item) to match the ItemID used in lookupTaxes + $quoteItemId = $item->getQuoteItemId(); + $itemId = $quoteItemId ? $item->getSku() . '-' . $quoteItemId : $item->getSku(); + $cartItems[] = array( - 'ItemID' => $item->getSku(), + 'ItemID' => $itemId, 'Index' => $index, 'TIC' => $this->productTicService->getProductTic($item, 'returnOrder'), 'Price' => $creditItem->getPrice() - $creditItem->getDiscountAmount() / $creditItem->getQty(), diff --git a/Test/Unit/Mocks/MagentoMocks.php b/Test/Unit/Mocks/MagentoMocks.php index a8ace2e..4bb58e8 100644 --- a/Test/Unit/Mocks/MagentoMocks.php +++ b/Test/Unit/Mocks/MagentoMocks.php @@ -69,6 +69,8 @@ public function getSku() { return null; } public function setSku($sku) { return $this; } public function getProduct() { return null; } public function setProduct($product) { return $this; } + public function getQuoteItemId() { return null; } + public function getId() { return null; } } class Order diff --git a/Test/Unit/Model/ApiTest.php b/Test/Unit/Model/ApiTest.php index d295b6d..b37e3c1 100644 --- a/Test/Unit/Model/ApiTest.php +++ b/Test/Unit/Model/ApiTest.php @@ -238,6 +238,7 @@ public function testReturnOrderWithCartItems() $creditItem->method('getQty')->willReturn(1); $orderItem->method('getSku')->willReturn('TEST_SKU'); + $orderItem->method('getQuoteItemId')->willReturn(12345); // Quote item ID for ItemID consistency $orderItem->method('getProduct')->willReturn($product); $product->method('getId')->willReturn(1); @@ -260,15 +261,19 @@ public function testReturnOrderWithCartItems() $this->mockSoapClient->method('Returned') ->willReturn($mockResponse); - // Mock data object methods - $this->mockDataObject->method('setParams')->willReturnSelf(); + // Mock data object methods - capture params to verify ItemID format + $capturedParams = null; + $this->mockDataObject->method('setParams')->willReturnCallback(function($params) use (&$capturedParams) { + $capturedParams = $params; + return $this->mockDataObject; + }); $this->mockDataObject->method('getParams')->willReturn([ 'apiLoginID' => 'test_api_id', 'apiKey' => 'test_api_key', 'orderID' => 'TEST_ORDER_123', 'cartItems' => [ [ - 'ItemID' => 'TEST_SKU', + 'ItemID' => 'TEST_SKU-12345', 'Index' => 0, 'TIC' => '20000', 'Price' => 14.99, @@ -296,6 +301,14 @@ public function testReturnOrderWithCartItems() // Assert the result $this->assertTrue($result, 'returnOrder should return true for successful refund with items'); + + // Verify ItemID format includes quote item ID for uniqueness + if ($capturedParams && isset($capturedParams['cartItems'][0]['ItemID'])) { + $itemId = $capturedParams['cartItems'][0]['ItemID']; + $this->assertStringContainsString('TEST_SKU', $itemId, 'ItemID should contain SKU'); + $this->assertStringContainsString('12345', $itemId, 'ItemID should contain quote item ID'); + $this->assertEquals('TEST_SKU-12345', $itemId, 'ItemID should be formatted as SKU-QuoteItemID'); + } } /** @@ -355,4 +368,135 @@ public function testReturnOrderFailsWhenParameterIsLost() // The test expects the method to return false due to the SOAP error $this->assertFalse($result, 'returnOrder should return false when returnCoDeliveryFeeWhenNoCartItems parameter is missing'); } + + public function testReturnOrderWithMultipleItemsSameSku() + { + // Mock configuration + $this->scopeConfig->method('getValue') + ->willReturnMap([ + ['tax/taxcloud_settings/enabled', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, null, '1'], + ['tax/taxcloud_settings/logging', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, null, '1'], + ['tax/taxcloud_settings/api_id', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, null, 'test_api_id'], + ['tax/taxcloud_settings/api_key', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, null, 'test_api_key'], + ['tax/taxcloud_settings/default_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, null, '00000'], + ['tax/taxcloud_settings/shipping_tic', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, null, '11010'] + ]); + + // Mock SOAP client + $this->soapClientFactory->method('create') + ->willReturn($this->mockSoapClient); + + // Mock data object for event handling + $this->objectFactory->method('create') + ->willReturn($this->mockDataObject); + + // Mock credit memo with multiple items that have the same SKU + // This simulates a regular item and a promo item scenario + $creditmemo = $this->createMock(\Magento\Sales\Model\Order\Creditmemo::class); + $order = $this->createMock(\Magento\Sales\Model\Order\Order::class); + $order->method('getIncrementId')->willReturn('TEST_ORDER_123'); + + // First item: regular item + $creditItem1 = $this->createMock(\Magento\Sales\Model\Order\Creditmemo\Item::class); + $orderItem1 = $this->createMock(\Magento\Sales\Model\Order\Item::class); + $product1 = $this->createMock(\Magento\Catalog\Model\Product::class); + + $creditItem1->method('getOrderItem')->willReturn($orderItem1); + $creditItem1->method('getPrice')->willReturn(120.00); + $creditItem1->method('getDiscountAmount')->willReturn(0); + $creditItem1->method('getQty')->willReturn(1); + + $orderItem1->method('getSku')->willReturn('ABC123'); + $orderItem1->method('getQuoteItemId')->willReturn(456); // Different quote item ID + $orderItem1->method('getProduct')->willReturn($product1); + + // Second item: promo item with same SKU + $creditItem2 = $this->createMock(\Magento\Sales\Model\Order\Creditmemo\Item::class); + $orderItem2 = $this->createMock(\Magento\Sales\Model\Order\Item::class); + $product2 = $this->createMock(\Magento\Catalog\Model\Product::class); + + $creditItem2->method('getOrderItem')->willReturn($orderItem2); + $creditItem2->method('getPrice')->willReturn(0.00); + $creditItem2->method('getDiscountAmount')->willReturn(0); + $creditItem2->method('getQty')->willReturn(1); + + $orderItem2->method('getSku')->willReturn('ABC123'); // Same SKU! + $orderItem2->method('getQuoteItemId')->willReturn(789); // Different quote item ID + $orderItem2->method('getProduct')->willReturn($product2); + + $product1->method('getId')->willReturn(1); + $product2->method('getId')->willReturn(1); + + $productModel = $this->createMock(\Magento\Catalog\Model\Product::class); + $customAttribute = $this->createMock(\Magento\Framework\Api\AttributeValue::class); + $productModel->method('load')->willReturnSelf(); + $productModel->method('getCustomAttribute')->willReturn($customAttribute); + $customAttribute->method('getValue')->willReturn('20000'); + + $this->productFactory->method('create')->willReturn($productModel); + + $creditmemo->method('getOrder')->willReturn($order); + $creditmemo->method('getAllItems')->willReturn([$creditItem1, $creditItem2]); + $creditmemo->method('getShippingAmount')->willReturn(0); + + // Mock successful SOAP response + $mockResponse = new \stdClass(); + $mockResponse->ReturnedResult = new \stdClass(); + $mockResponse->ReturnedResult->ResponseType = 'OK'; + $mockResponse->ReturnedResult->Messages = []; + + $this->mockSoapClient->method('Returned') + ->willReturn($mockResponse); + + // Mock data object methods - capture params to verify ItemID uniqueness + $capturedParams = null; + $this->mockDataObject->method('setParams')->willReturnCallback(function($params) use (&$capturedParams) { + $capturedParams = $params; + return $this->mockDataObject; + }); + $this->mockDataObject->method('getParams')->willReturn([ + 'apiLoginID' => 'test_api_id', + 'apiKey' => 'test_api_key', + 'orderID' => 'TEST_ORDER_123', + 'cartItems' => [ + [ + 'ItemID' => 'ABC123-456', + 'Index' => 0, + 'TIC' => '20000', + 'Price' => 120.00, + 'Qty' => 1 + ], + [ + 'ItemID' => 'ABC123-789', + 'Index' => 1, + 'TIC' => '20000', + 'Price' => 0.00, + 'Qty' => 1 + ] + ], + 'returnedDate' => '2025-01-03T00:00:00+00:00', + 'returnCoDeliveryFeeWhenNoCartItems' => false + ]); + $this->mockDataObject->method('setResult')->willReturnSelf(); + $this->mockDataObject->method('getResult')->willReturn([ + 'ResponseType' => 'OK', + 'Messages' => [] + ]); + + // Execute the method + $result = $this->api->returnOrder($creditmemo); + + // Assert the result + $this->assertTrue($result, 'returnOrder should return true for successful refund with multiple items'); + + // Verify ItemIDs are unique even with same SKU + if ($capturedParams && isset($capturedParams['cartItems'][0]['ItemID']) && isset($capturedParams['cartItems'][1]['ItemID'])) { + $itemId1 = $capturedParams['cartItems'][0]['ItemID']; + $itemId2 = $capturedParams['cartItems'][1]['ItemID']; + + $this->assertNotEquals($itemId1, $itemId2, 'Items with same SKU should have unique ItemIDs'); + $this->assertEquals('ABC123-456', $itemId1, 'First item should have ItemID with quote item ID 456'); + $this->assertEquals('ABC123-789', $itemId2, 'Second item should have ItemID with quote item ID 789'); + } + } } \ No newline at end of file