diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al index 3476ce46f0..f1337c048d 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al @@ -657,6 +657,10 @@ codeunit 30178 "Shpfy Product Export" UpdateProductVariant(ShopifyVariant, Item, ItemVariant, TempCurrVariant); end; until ShopifyVariant.Next() = 0; + // Re-fetch the parent product's item, as the loop above may have overwritten + // the Item variable with a child item from "Add Item as Shopify Variant". + if not Item.GetBySystemId(ShopifyProduct."Item SystemId") then + exit; ItemVariant.SetRange("Item No.", Item."No."); ItemUnitofMeasure.SetRange("Item No.", Item."No."); if ItemVariant.FindSet(false) then diff --git a/src/Apps/W1/Shopify/Test/.resources/Products/ProductUpdateResponse.txt b/src/Apps/W1/Shopify/Test/.resources/Products/ProductUpdateResponse.txt new file mode 100644 index 0000000000..407fb106f4 --- /dev/null +++ b/src/Apps/W1/Shopify/Test/.resources/Products/ProductUpdateResponse.txt @@ -0,0 +1 @@ +{"data":{"productUpdate":{"product":{"onlineStoreUrl":"","onlineStorePreviewUrl":"","updatedAt":"2026-01-01T00:00:00Z"},"userErrors":[]}}} \ No newline at end of file diff --git a/src/Apps/W1/Shopify/Test/.resources/Products/ProductVariantsBulkUpdateResponse.txt b/src/Apps/W1/Shopify/Test/.resources/Products/ProductVariantsBulkUpdateResponse.txt new file mode 100644 index 0000000000..e27c80a41b --- /dev/null +++ b/src/Apps/W1/Shopify/Test/.resources/Products/ProductVariantsBulkUpdateResponse.txt @@ -0,0 +1 @@ +{"data":{"productVariantsBulkUpdate":{"productVariants":[],"userErrors":[]}}} \ No newline at end of file diff --git a/src/Apps/W1/Shopify/Test/Products/ShpfyCreateProductTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Products/ShpfyCreateProductTest.Codeunit.al index 415db40208..81e43dac55 100644 --- a/src/Apps/W1/Shopify/Test/Products/ShpfyCreateProductTest.Codeunit.al +++ b/src/Apps/W1/Shopify/Test/Products/ShpfyCreateProductTest.Codeunit.al @@ -15,11 +15,17 @@ codeunit 139601 "Shpfy Create Product Test" Subtype = Test; TestType = IntegrationTest; TestPermissions = Disabled; + TestHttpRequestPolicy = BlockOutboundRequests; var + ExportShop: Record "Shpfy Shop"; Any: Codeunit Any; LibraryAssert: Codeunit "Library Assert"; + OutboundHttpRequests: Codeunit "Library - Variable Storage"; + LibraryRandom: Codeunit "Library - Random"; + ShpfyInitializeTest: Codeunit "Shpfy Initialize Test"; + ExportIsInitialized: Boolean; [Test] procedure UnitTestCreateTempProductFromItem() @@ -2958,4 +2964,150 @@ codeunit 139601 "Shpfy Create Product Test" until TempShopifyVariant.Next() = 0; end; + + [Test] + [HandlerFunctions('ProductExportChildItemVariantHttpHandler')] + procedure UnitTestProductExportDoesNotCreateVariantsForChildItemVariants() + var + ParentItem: Record Item; + ChildItem: Record Item; + ChildItemVariantMapped: Record "Item Variant"; + ChildItemVariantUnmapped: Record "Item Variant"; + ShopifyProduct: Record "Shpfy Product"; + ShopifyVariant: Record "Shpfy Variant"; + ProductExport: Codeunit "Shpfy Product Export"; + ProductInitTest: Codeunit "Shpfy Product Init Test"; + begin + // [SCENARIO] Product Export must not create additional Shopify variants + // [SCENARIO] for unmapped child-item variants when a child item was added as Shopify variant. + InitializeProductExport(); + + // [GIVEN] Register Expected Outbound API Requests. + RegExpectedOutboundHttpRequestsForProductExport(); + + // [GIVEN] A parent item (without BC variants) and a child item with two BC variants. + ParentItem := ProductInitTest.CreateItem(ExportShop."Item Templ. Code", Any.DecimalInRange(10, 100, 2), Any.DecimalInRange(100, 500, 2), false); + ChildItem := ProductInitTest.CreateItem(ExportShop."Item Templ. Code", Any.DecimalInRange(10, 100, 2), Any.DecimalInRange(100, 500, 2), false); + ChildItemVariantMapped := CreateItemVariantForExport(ChildItem, 'MAP'); + ChildItemVariantUnmapped := CreateItemVariantForExport(ChildItem, 'UNMAPPED'); + + // [GIVEN] A Shopify product mapped to the parent item and one existing Shopify variant + // [GIVEN] mapped to only one child item variant. + ShopifyProduct := CreateShopifyProductForExport(ParentItem.SystemId); + ShopifyVariant := CreateMappedShopifyVariantForExport(ShopifyProduct.Id, ChildItem.SystemId, ChildItemVariantMapped.SystemId); + + // [WHEN] Product export runs for the shop. + ProductExport.SetShop(ExportShop); + ExportShop.SetRange(Code, ExportShop.Code); + ProductExport.Run(ExportShop); + OutboundHttpRequests.AssertEmpty(); + + // [THEN] No new Shopify variant record is created for the unmapped child item variant. + ShopifyVariant.Reset(); + ShopifyVariant.SetRange("Product Id", ShopifyProduct.Id); + ShopifyVariant.SetRange("Item SystemId", ChildItem.SystemId); + ShopifyVariant.SetRange("Item Variant SystemId", ChildItemVariantUnmapped.SystemId); + LibraryAssert.IsTrue(ShopifyVariant.IsEmpty(), 'Unexpected Shopify variant record created for unmapped child item variant.'); + end; + + [HttpClientHandler] + internal procedure ProductExportChildItemVariantHttpHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean + var + ProductUpdateResponseTok: Label 'Products/ProductUpdateResponse.txt', Locked = true; + ProductVariantsBulkUpdateResponseTok: Label 'Products/ProductVariantsBulkUpdateResponse.txt', Locked = true; + UnexpectedAPICallsErr: Label 'More than expected API calls to Shopify detected.'; + begin + if not ShpfyInitializeTest.VerifyRequestUrl(Request.Path, ExportShop."Shopify URL") then + exit(true); + + case OutboundHttpRequests.Length() of + 2: + LoadProductExportResourceIntoHttpResponse(ProductUpdateResponseTok, Response); + 1: + LoadProductExportResourceIntoHttpResponse(ProductVariantsBulkUpdateResponseTok, Response); + 0: + Error(UnexpectedAPICallsErr); + end; + exit(false); + end; + + local procedure InitializeProductExport() + var + CommunicationMgt: Codeunit "Shpfy Communication Mgt."; + AccessToken: SecretText; + begin + Any.SetDefaultSeed(); + OutboundHttpRequests.Clear(); + // CreateShop() sets IsTestInProgress = true on the singleton CommunicationMgt, + // which redirects HTTP calls through event mocking instead of HttpClient.Send(). + // Disable that so [HttpClientHandler] can intercept the requests instead. + CommunicationMgt.SetTestInProgress(false); + if ExportIsInitialized then + exit; + + ExportShop := ShpfyInitializeTest.CreateShop(); + ExportShop."Can Update Shopify Products" := true; + ExportShop."Product Metafields To Shopify" := false; + ExportShop.Modify(); + Commit(); + + AccessToken := LibraryRandom.RandText(20); + ShpfyInitializeTest.RegisterAccessTokenForShop(ExportShop.GetStoreName(), AccessToken); + + ExportIsInitialized := true; + end; + + local procedure RegExpectedOutboundHttpRequestsForProductExport() + begin + OutboundHttpRequests.Enqueue('GQL Update Product'); + OutboundHttpRequests.Enqueue('GQL Update Product Variants'); + end; + + local procedure LoadProductExportResourceIntoHttpResponse(ResourceText: Text; var Response: TestHttpResponseMessage) + begin + Response.Content.WriteFrom(NavApp.GetResourceAsText(ResourceText, TextEncoding::UTF8)); + OutboundHttpRequests.DequeueText(); + end; + + local procedure CreateItemVariantForExport(Item: Record Item; VariantCodePrefix: Text): Record "Item Variant" + var + ItemVariant: Record "Item Variant"; + begin + ItemVariant.Init(); + ItemVariant.Validate("Item No.", Item."No."); + ItemVariant.Code := CopyStr(VariantCodePrefix + Any.AlphabeticText(5), 1, MaxStrLen(ItemVariant.Code)); + ItemVariant.Description := CopyStr(Any.AlphabeticText(20), 1, MaxStrLen(ItemVariant.Description)); + ItemVariant.Insert(); + exit(ItemVariant); + end; + + local procedure CreateShopifyProductForExport(ItemSystemId: Guid): Record "Shpfy Product" + var + ShopifyProduct: Record "Shpfy Product"; + begin + ShopifyProduct.Init(); + ShopifyProduct.Id := Any.IntegerInRange(10000, 99999); + ShopifyProduct."Shop Code" := ExportShop.Code; + ShopifyProduct."Item SystemId" := ItemSystemId; + ShopifyProduct.Title := CopyStr(Any.AlphabeticText(20), 1, MaxStrLen(ShopifyProduct.Title)); + ShopifyProduct.Insert(); + exit(ShopifyProduct); + end; + + local procedure CreateMappedShopifyVariantForExport(ProductId: BigInteger; ItemSystemId: Guid; ItemVariantSystemId: Guid): Record "Shpfy Variant" + var + ShopifyVariant: Record "Shpfy Variant"; + begin + ShopifyVariant.Init(); + ShopifyVariant.Id := Any.IntegerInRange(100000, 999999); + ShopifyVariant."Shop Code" := ExportShop.Code; + ShopifyVariant."Product Id" := ProductId; + ShopifyVariant."Item SystemId" := ItemSystemId; + ShopifyVariant."Item Variant SystemId" := ItemVariantSystemId; + ShopifyVariant."Option 1 Name" := 'Variant'; + ShopifyVariant."Option 1 Value" := CopyStr(Any.AlphabeticText(10), 1, MaxStrLen(ShopifyVariant."Option 1 Value")); + ShopifyVariant.Title := CopyStr(Any.AlphabeticText(20), 1, MaxStrLen(ShopifyVariant.Title)); + ShopifyVariant.Insert(); + exit(ShopifyVariant); + end; }