diff --git a/src/Apps/W1/Shopify/App/src/Base/Tables/ShpfyShop.Table.al b/src/Apps/W1/Shopify/App/src/Base/Tables/ShpfyShop.Table.al index 081b40ecda..9a9851a3d9 100644 --- a/src/Apps/W1/Shopify/App/src/Base/Tables/ShpfyShop.Table.al +++ b/src/Apps/W1/Shopify/App/src/Base/Tables/ShpfyShop.Table.al @@ -5,6 +5,7 @@ namespace Microsoft.Integration.Shopify; +using Microsoft.Inventory.Item.Attribute; using Microsoft.Finance.Currency; using Microsoft.Finance.GeneralLedger.Account; using Microsoft.Finance.GeneralLedger.Setup; @@ -300,6 +301,9 @@ table 30102 "Shpfy Shop" trigger OnValidate() begin + if "UoM as Variant" then + VerifyNoItemAttributesAsOptions(); + if "UoM as Variant" and ("Option Name for UoM" = '') then "Option Name for UoM" := 'Unit of Measure'; end; @@ -1090,4 +1094,14 @@ table 30102 "Shpfy Shop" end; #pragma warning restore AL0432 #endif + + local procedure VerifyNoItemAttributesAsOptions() + var + ItemAttribute: Record "Item Attribute"; + UoMVariantUnavailableErr: Label 'UoM as Variant is unavailable due to existing Item Attributes marked as “As Option” which are utilized for Shopify Product Options.'; + begin + ItemAttribute.SetRange("Shpfy Incl. in Product Sync", "Shpfy Incl. in Product Sync"::"As Option"); + if not ItemAttribute.IsEmpty() then + Error(UoMVariantUnavailableErr); + end; } diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al index 6ecec599a4..cda6a4141d 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateItemAsVariant.Codeunit.al @@ -34,6 +34,9 @@ codeunit 30343 "Shpfy Create Item As Variant" internal procedure CreateVariantFromItem(var Item: Record "Item") var TempShopifyVariant: Record "Shpfy Variant" temporary; + ProductExport: Codeunit "Shpfy Product Export"; + VariantOptionTok: Label 'Variant', Locked = true; + TitleOptionTok: Label 'Title', Locked = true; begin if Item.SystemId = ShopifyProduct."Item SystemId" then exit; @@ -41,14 +44,20 @@ codeunit 30343 "Shpfy Create Item As Variant" CreateProduct.CreateTempShopifyVariantFromItem(Item, TempShopifyVariant); TempShopifyVariant.Title := Item."No."; - if not ShopifyProduct."Has Variants" and (OptionName = 'Title') then begin + if not ShopifyProduct."Has Variants" and (OptionName = TitleOptionTok) then begin // Shopify automatically deletes the default variant (Title) when adding a new one so first we need to update the default variant to have a different name (Variant) - UpdateProductOption('Variant'); - TempShopifyVariant."Option 1 Name" := 'Variant'; + UpdateProductOption(VariantOptionTok); + TempShopifyVariant."Option 1 Name" := VariantOptionTok; end else TempShopifyVariant."Option 1 Name" := CopyStr(OptionName, 1, MaxStrLen(TempShopifyVariant."Option 1 Name")); TempShopifyVariant."Option 1 Value" := Item."No."; + + if ShopifyProduct."Has Variants" and (OptionName <> VariantOptionTok) then begin + ProductExport.SetShop(Shop); + ProductExport.ValidateItemAttributesAsProductOptionsForNewVariant(TempShopifyVariant, Item, '', ShopifyProduct.Id); + end; + Events.OnAfterCreateTempShopifyVariant(Item, TempShopifyVariant); TempShopifyVariant.Modify(); @@ -67,7 +76,6 @@ codeunit 30343 "Shpfy Create Item As Variant" var CommunicationMgt: Codeunit "Shpfy Communication Mgt."; Options: Dictionary of [Text, Text]; - MultipleOptionsErr: Label 'The product has more than one option. Items cannot be added as variants to a product with multiple options.'; UOMAsVariantEnabledErr: Label 'Items cannot be added as variants to a product with the "%1" setting enabled for this store.', Comment = '%1 - UoM as Variant field caption'; begin if Shop."UoM as Variant" then @@ -75,9 +83,6 @@ codeunit 30343 "Shpfy Create Item As Variant" Options := ProductApi.GetProductOptions(ShopifyProduct.Id); - if Options.Count > 1 then - Error(MultipleOptionsErr); - OptionId := CommunicationMgt.GetIdOfGId(Options.Keys.Get(1)); OptionName := Options.Values.Get(1); end; diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al index f690c24b19..3495495a37 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al @@ -57,6 +57,9 @@ codeunit 30174 "Shpfy Create Product" TempShopifyVariant: Record "Shpfy Variant" temporary; TempShopifyTag: Record "Shpfy Tag" temporary; begin + if not ProductExport.CheckItemAttributesCompatibleForProductOptions(Item) then + exit; + CreateTempProduct(Item, TempShopifyProduct, TempShopifyVariant, TempShopifyTag); if not VariantApi.FindShopifyProductVariant(TempShopifyProduct, TempShopifyVariant) then ProductId := ProductApi.CreateProduct(TempShopifyProduct, TempShopifyVariant, TempShopifyTag) @@ -164,6 +167,7 @@ codeunit 30174 "Shpfy Create Product" end else CreateTempShopifyVariantFromItem(Item, TempShopifyVariant); + ProductExport.FillProductOptionsForShopifyVariants(Item, TempShopifyVariant, TempShopifyProduct); TempShopifyProduct.Insert(false); Events.OnAfterCreateTempShopifyProduct(Item, TempShopifyProduct, TempShopifyVariant, TempShopifyTag); end; 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 208621e2ac..5d5035f769 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 @@ -27,6 +27,7 @@ codeunit 30178 "Shpfy Product Export" tabledata "Item Attribute Translation" = r, tabledata "Item Attribute Value" = r, tabledata "Item Attribute Value Mapping" = r, + tabledata "Item Var. Attr. Value Mapping" = r, tabledata "Item Category" = r, tabledata "Item Reference" = r, tabledata "Item Unit of Measure" = rim, @@ -241,7 +242,7 @@ codeunit 30178 "Shpfy Product Export" /// /// Parameter of type Record Item. /// Parameter of type Record "Item Variant". - local procedure CreateProductVariant(ProductId: BigInteger; Item: Record Item; ItemVariant: Record "Item Variant") + local procedure CreateProductVariant(ProductId: BigInteger; Item: Record Item; ItemVariant: Record "Item Variant"; TempShopifyProduct: Record "Shpfy Product" temporary) var TempShopifyVariant: Record "Shpfy Variant" temporary; begin @@ -254,6 +255,10 @@ codeunit 30178 "Shpfy Product Export" exit; Clear(TempShopifyVariant); FillInProductVariantData(TempShopifyVariant, Item, ItemVariant); + + if not ValidateItemAttributesAsProductOptionsForNewVariant(TempShopifyVariant, Item, ItemVariant.Code, TempShopifyProduct.Id) then + exit; + TempShopifyVariant.Insert(false); VariantApi.AddProductVariant(TempShopifyVariant, ProductId, "Shpfy Variant Create Strategy"::DEFAULT); end; @@ -420,10 +425,11 @@ codeunit 30178 "Shpfy Product Export" ShopifyVariant.Weight := Item."Gross Weight"; if ShopifyVariant."Option 1 Name" = '' then ShopifyVariant."Option 1 Name" := 'Variant'; - if ItemAsVariant then - ShopifyVariant."Option 1 Value" := Item."No." - else - ShopifyVariant."Option 1 Value" := ItemVariant.Code; + if ShopifyVariant."Option 1 Name" = 'Variant' then + if ItemAsVariant then + ShopifyVariant."Option 1 Value" := Item."No." + else + ShopifyVariant."Option 1 Value" := ItemVariant.Code; ShopifyVariant."Shop Code" := Shop.Code; ShopifyVariant."Item SystemId" := Item.SystemId; ShopifyVariant."Item Variant SystemId" := ItemVariant.SystemId; @@ -704,7 +710,7 @@ codeunit 30178 "Shpfy Product Export" if ShopifyVariant.FindFirst() then UpdateProductVariant(ShopifyVariant, Item, ItemVariant, TempCurrVariant) else - CreateProductVariant(ProductId, Item, ItemVariant); + CreateProductVariant(ProductId, Item, ItemVariant, TempShopifyProduct); until ItemVariant.Next() = 0 else begin Clear(ShopifyVariant); @@ -914,4 +920,395 @@ codeunit 30178 "Shpfy Product Export" until ShopifyLanguage.Next() = 0; end; #endregion + + #region Shopify Product Options as Item/Variant Attributes + /// + /// Checks if item/item variant attributes marked as "As Option" are compatible to be used as product options in Shopify. + /// + /// The item to check. + /// True if the item attributes are compatible, false otherwise. + internal procedure CheckItemAttributesCompatibleForProductOptions(Item: Record Item): Boolean + var + ItemAttributeIds: List of [Integer]; + SkippedReason: Text[250]; + TooManyAttributesAsOptionErr: Label 'Item %1 has %2 attributes marked as "As Option". Shopify supports a maximum of 3 product options.', Comment = '%1 = Item No., %2 = Number of attributes'; + DuplicateOptionCombinationErr: Label 'Item %1 has duplicate item variant attribute value combinations. Each variant must have a unique combination of option values.', Comment = '%1 = Item No.'; + begin + if Shop."UoM as Variant" then + exit(true); + + GetItemAttributeIDsMarkedAsOption(Item, ItemAttributeIds); + + if ItemAttributeIds.Count() = 0 then + exit(true); + + if ItemAttributeIds.Count() > 3 then begin + SkippedRecord.LogSkippedRecord(Item.RecordId, StrSubstNo(TooManyAttributesAsOptionErr, Item."No.", ItemAttributeIds.Count()), Shop); + exit(false); + end; + + if CheckMissingItemAttributeValues(Item, ItemAttributeIds, SkippedReason) then begin + SkippedRecord.LogSkippedRecord(Item.RecordId, SkippedReason, Shop); + exit(false); + end; + + if CheckProductOptionDuplicatesExists(Item, ItemAttributeIds) then begin + SkippedRecord.LogSkippedRecord(Item.RecordId, StrSubstNo(DuplicateOptionCombinationErr, Item."No."), Shop); + exit(false); + end; + + exit(true); + end; + + local procedure GetItemAttributeIDsMarkedAsOption(Item: Record Item; var ItemAttributeIds: List of [Integer]) + var + ItemAttribute: Record "Item Attribute"; + ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; + begin + ItemAttributeValueMapping.SetRange("Table ID", Database::Item); + ItemAttributeValueMapping.SetRange("No.", Item."No."); + if ItemAttributeValueMapping.FindSet() then + repeat + if ItemAttribute.Get(ItemAttributeValueMapping."Item Attribute ID") then + if (not ItemAttribute.Blocked) and (ItemAttribute."Shpfy Incl. in Product Sync" = ItemAttribute."Shpfy Incl. in Product Sync"::"As Option") then + if not ItemAttributeIds.Contains(ItemAttribute.ID) then + ItemAttributeIds.Add(ItemAttribute.ID); + until ItemAttributeValueMapping.Next() = 0; + end; + + local procedure CheckProductOptionDuplicatesExists(Item: Record Item; ItemAttributeIds: List of [Integer]): Boolean + var + ItemAttributeValue: Record "Item Attribute Value"; + ItemVariant: Record "Item Variant"; + ItemVarAttrValueMapping: Record "Item Var. Attr. Value Mapping"; + VariantCombinations: Dictionary of [Text, Code[10]]; + CombinationKey: Text; + VariantCode: Code[10]; + AttributeId: Integer; + CombinationKeyTok: Label '%1:%2|', Locked = true, Comment = '%1 = Attribute ID, %2 = Attribute Value'; + begin + ItemVariant.SetRange("Item No.", Item."No."); + if not ItemVariant.FindSet() then + exit(false); + + repeat + VariantCode := ItemVariant.Code; + + CombinationKey := ''; + foreach AttributeId in ItemAttributeIds do begin + ItemVarAttrValueMapping.SetRange("Item No.", Item."No."); + ItemVarAttrValueMapping.SetRange("Variant Code", VariantCode); + ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); + if ItemVarAttrValueMapping.FindFirst() then + if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then + CombinationKey += StrSubstNo(CombinationKeyTok, ItemAttributeValue."Attribute ID", ItemAttributeValue.Value); + end; + + if CombinationKey <> '' then + if VariantCombinations.ContainsKey(CombinationKey) then + exit(true) + else + VariantCombinations.Add(CombinationKey, VariantCode); + until ItemVariant.Next() = 0; + end; + + local procedure CheckMissingItemAttributeValues(Item: Record Item; ItemAttributeIds: List of [Integer]; var SkippedReason: Text[250]): Boolean + var + ItemAttribute: Record "Item Attribute"; + ItemVariant: Record "Item Variant"; + ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; + ItemVarAttrValueMapping: Record "Item Var. Attr. Value Mapping"; + ItemAttributeValue: Record "Item Attribute Value"; + AttributeId: Integer; + MissingVariantCode: Code[10]; + MissingAttributeName: Text[250]; + MissingAttributeErr: Label 'Item %1 Variant %2 is missing an attribute "%3". All item variants must have must have item attributes marked as "As Option".', Comment = '%1 = Item No., %2 = Variant Code, %3 = Attribute Name'; + MissingItemAttributeValueErr: Label 'Item %1 is missing a value for attribute "%2". Item must have values for attributes marked as "As Option".', Comment = '%1 = Item No., %2 = Attribute Name'; + MissingItemVarAttributeValueErr: Label 'Item %1 Variant %2 is missing a value for attribute "%3". All item variants must have values for attributes marked as "As Option".', Comment = '%1 = Item No., %2 = Variant Code, %3 = Attribute Name'; + begin + ItemVariant.SetRange("Item No.", Item."No."); + if ItemVariant.FindSet() then + repeat + foreach AttributeId in ItemAttributeIds do begin + ItemVarAttrValueMapping.SetRange("Item No.", Item."No."); + ItemVarAttrValueMapping.SetRange("Variant Code", ItemVariant.Code); + ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); + if not ItemVarAttrValueMapping.FindFirst() then begin + MissingVariantCode := ItemVariant.Code; + if ItemAttribute.Get(AttributeId) then + MissingAttributeName := ItemAttribute.Name; + SkippedReason := StrSubstNo(MissingAttributeErr, Item."No.", MissingVariantCode, MissingAttributeName); + exit(true); + end else + if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then + if ItemAttributeValue.Value = '' then begin + MissingVariantCode := ItemVariant.Code; + if ItemAttribute.Get(AttributeId) then + MissingAttributeName := ItemAttribute.Name; + SkippedReason := StrSubstNo(MissingItemVarAttributeValueErr, Item."No.", MissingVariantCode, MissingAttributeName); + exit(true); + end; + end; + until ItemVariant.Next() = 0 + else + foreach AttributeId in ItemAttributeIds do begin + ItemAttributeValueMapping.SetRange("Table ID", Database::Item); + ItemAttributeValueMapping.SetRange("No.", Item."No."); + ItemAttributeValueMapping.SetRange("Item Attribute ID", AttributeId); + if ItemAttributeValueMapping.FindFirst() then + if ItemAttributeValue.Get(ItemAttributeValueMapping."Item Attribute ID", ItemAttributeValueMapping."Item Attribute Value ID") then + if ItemAttributeValue.Value = '' then begin + if ItemAttribute.Get(AttributeId) then + MissingAttributeName := ItemAttribute.Name; + SkippedReason := StrSubstNo(MissingItemAttributeValueErr, Item."No.", MissingAttributeName); + exit(true); + end; + end; + end; + + /// + /// Fills product options for Shopify variants based on item attributes marked as "As Option". + /// + /// The item to process. + /// Parameter of Shopify Variants to fill. + /// Parameter of Shopify Product. + internal procedure FillProductOptionsForShopifyVariants(Item: Record Item; var TempShopifyVariant: Record "Shpfy Variant" temporary; var TempShopifyProduct: Record "Shpfy Product" temporary) + var + ItemVariant: Record "Item Variant"; + ItemAttributeIds: List of [Integer]; + VariantCode: Code[10]; + begin + if Shop."UoM as Variant" then + exit; + + GetItemAttributeIDsMarkedAsOption(Item, ItemAttributeIds); + + if ItemAttributeIds.Count() = 0 then + exit; + + if TempShopifyVariant.FindSet() then + repeat + VariantCode := ''; + if not IsNullGuid(TempShopifyVariant."Item Variant SystemId") then + if ItemVariant.GetBySystemId(TempShopifyVariant."Item Variant SystemId") then + VariantCode := ItemVariant.Code; + + FillProductOptionsFromItemAttributes(Item."No.", VariantCode, ItemAttributeIds, TempShopifyVariant); + TempShopifyVariant.Modify(false); + until TempShopifyVariant.Next() = 0; + + TempShopifyProduct."Has Variants" := true; + end; + + local procedure FillProductOptionsFromItemAttributes(ItemNo: Code[20]; VariantCode: Code[10]; ItemAttributeIds: List of [Integer]; var TempShopifyVariant: Record "Shpfy Variant" temporary) + var + ItemAttribute: Record "Item Attribute"; + ItemAttributeValue: Record "Item Attribute Value"; + ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; + ItemVarAttrValueMapping: Record "Item Var. Attr. Value Mapping"; + OptionIndex: Integer; + AttributeId: Integer; + begin + OptionIndex := 1; + foreach AttributeId in ItemAttributeIds do + if ItemAttribute.Get(AttributeId) then begin + if VariantCode <> '' then begin + ItemVarAttrValueMapping.SetRange("Item No.", ItemNo); + ItemVarAttrValueMapping.SetRange("Variant Code", VariantCode); + ItemVarAttrValueMapping.SetRange("Item Attribute ID", AttributeId); + if ItemVarAttrValueMapping.FindFirst() then + if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then + AssignProductOptionValues(TempShopifyVariant, OptionIndex, ItemAttribute.Name, ItemAttributeValue.Value); + end else begin + ItemAttributeValueMapping.SetRange("Table ID", Database::Item); + ItemAttributeValueMapping.SetRange("No.", ItemNo); + ItemAttributeValueMapping.SetRange("Item Attribute ID", AttributeId); + if ItemAttributeValueMapping.FindFirst() then + if ItemAttributeValue.Get(ItemAttributeValueMapping."Item Attribute ID", ItemAttributeValueMapping."Item Attribute Value ID") then + AssignProductOptionValues(TempShopifyVariant, OptionIndex, ItemAttribute.Name, ItemAttributeValue.Value); + end; + OptionIndex += 1; + end; + end; + + local procedure AssignProductOptionValues(var TempShopifyVariant: Record "Shpfy Variant" temporary; OptionIndex: Integer; AttributeName: Text[250]; AttributeValue: Text[250]) + begin + case OptionIndex of + 1: + begin + TempShopifyVariant."Option 1 Name" := CopyStr(AttributeName, 1, MaxStrLen(TempShopifyVariant."Option 1 Name")); + TempShopifyVariant."Option 1 Value" := CopyStr(AttributeValue, 1, MaxStrLen(TempShopifyVariant."Option 1 Value")); + end; + 2: + begin + TempShopifyVariant."Option 2 Name" := CopyStr(AttributeName, 1, MaxStrLen(TempShopifyVariant."Option 2 Name")); + TempShopifyVariant."Option 2 Value" := CopyStr(AttributeValue, 1, MaxStrLen(TempShopifyVariant."Option 2 Value")); + end; + 3: + begin + TempShopifyVariant."Option 3 Name" := CopyStr(AttributeName, 1, MaxStrLen(TempShopifyVariant."Option 3 Name")); + TempShopifyVariant."Option 3 Value" := CopyStr(AttributeValue, 1, MaxStrLen(TempShopifyVariant."Option 3 Value")); + end; + end; + end; + + + /// + /// Validates item attributes and prepares temporary Shopify variant by assigning product option values from Item/Item Variant. + /// + /// Parameter of type Record "Shpfy Variant" temporary. + /// Parameter of type Record Item. + /// Parameter of type Record "Item Variant". + /// Parameter of type BigInteger. + internal procedure ValidateItemAttributesAsProductOptionsForNewVariant(var TempShopifyVariant: Record "Shpfy Variant" temporary; Item: Record Item; ItemVariantCode: Code[10]; ShopifyProductId: BigInteger): Boolean + var + ItemAttributeIds: List of [Integer]; + ProductOptions: Dictionary of [Integer, Text]; + ExistingProductOptionValues: Dictionary of [Text, Text]; + ProductOptionIndex: Integer; + begin + if Shop."UoM as Variant" then + exit(true); + + GetItemAttributeIDsMarkedAsOption(Item, ItemAttributeIds); + + if ItemAttributeIds.Count() = 0 then + exit(true); + + CollectExistingProductVariantOptionValues(ProductOptions, ExistingProductOptionValues, ShopifyProductId); + + for ProductOptionIndex := 1 to ProductOptions.Count() do + if not AssignProductOptionValuesToTempProductVariant(TempShopifyVariant, Item, ItemVariantCode, ProductOptions, ProductOptionIndex) then + exit(false); + + if not CheckProductOptionsCombinationUnique(TempShopifyVariant, ExistingProductOptionValues, Item, ItemVariantCode) then + exit(false); + + exit(true); + end; + + local procedure CollectExistingProductVariantOptionValues(var ProductOptions: Dictionary of [Integer, Text]; var ExistingProductOptionValues: Dictionary of [Text, Text]; ShopifyProductId: BigInteger) + var + ShopifyVariant: Record "Shpfy Variant"; + ItemAttribute: Record "Item Attribute"; + CombinationKey: Text; + begin + ShopifyVariant.SetAutoCalcFields("Variant Code"); + ShopifyVariant.SetRange("Product Id", ShopifyProductId); + if ShopifyVariant.FindSet() then + repeat + CombinationKey := BuildCombinationKey( + ShopifyVariant."Option 1 Name", ShopifyVariant."Option 1 Value", + ShopifyVariant."Option 2 Name", ShopifyVariant."Option 2 Value", + ShopifyVariant."Option 3 Name", ShopifyVariant."Option 3 Value"); + + if not ExistingProductOptionValues.ContainsKey(CombinationKey) then + ExistingProductOptionValues.Add(CombinationKey, ShopifyVariant."Variant Code"); + until ShopifyVariant.Next() = 0; + + if ShopifyVariant."Option 1 Name" <> '' then begin + ItemAttribute.SetRange(Name, ShopifyVariant."Option 1 Name"); + if ItemAttribute.FindFirst() then + ProductOptions.Add(ItemAttribute.ID, ShopifyVariant."Option 1 Name"); + end; + if ShopifyVariant."Option 2 Name" <> '' then begin + ItemAttribute.SetRange(Name, ShopifyVariant."Option 2 Name"); + if ItemAttribute.FindFirst() then + ProductOptions.Add(ItemAttribute.ID, ShopifyVariant."Option 2 Name"); + end; + if ShopifyVariant."Option 3 Name" <> '' then begin + ItemAttribute.SetRange(Name, ShopifyVariant."Option 3 Name"); + if ItemAttribute.FindFirst() then + ProductOptions.Add(ItemAttribute.ID, ShopifyVariant."Option 3 Name"); + end; + end; + + local procedure BuildCombinationKey(Option1Name: Text; Option1Value: Text; Option2Name: Text; Option2Value: Text; Option3Name: Text; Option3Value: Text): Text + var + CombinationKey: Text; + KeyPartTok: Label '%1:%2|', Locked = true, Comment = '%1 = Option Name, %2 = Option Value'; + begin + if Option1Name <> '' then + CombinationKey += StrSubstNo(KeyPartTok, Option1Name, Option1Value); + if Option2Name <> '' then + CombinationKey += StrSubstNo(KeyPartTok, Option2Name, Option2Value); + if Option3Name <> '' then + CombinationKey += StrSubstNo(KeyPartTok, Option3Name, Option3Value); + + exit(CombinationKey); + end; + + local procedure AssignProductOptionValuesToTempProductVariant(var TempShopifyVariant: Record "Shpfy Variant" temporary; Item: Record "Item"; ItemVariantCode: Code[10]; ProductOptions: Dictionary of [Integer, Text]; ProductOptionIndex: Integer): Boolean + var + ItemVariant: Record "Item Variant"; + ItemAttributeValueMapping: Record "Item Attribute Value Mapping"; + ItemVarAttrValueMapping: Record "Item Var. Attr. Value Mapping"; + ItemAttributeValue: Record "Item Attribute Value"; + ItemWithoutRequiredAttributeErr: Label 'Item %1 cannot be added as a product variant because it does not have required attributes.', Comment = '%1 = Item No.'; + ItemWithoutRequiredAttributeValueErr: Label 'Item %1 cannot be added as a product variant because it does not have a value for the required attributes.', Comment = '%1 = Item No.'; + ItemVariantWithoutRequiredAttributeErr: Label 'Item Variant %1 cannot be added as a product variant because it does not have required attributes.', Comment = '%1 = Item No.'; + ItemVariantWithoutRequiredAttributeValueErr: Label 'Item Variant %1 cannot be added as a product variant because it does not have a value for the required attributes.', Comment = '%1 = Item No.'; + begin + ItemVariant.SetRange("Item No.", Item."No."); + ItemVariant.SetRange("Code", ItemVariantCode); + if ItemVariant.FindFirst() then begin + ItemVarAttrValueMapping.SetRange("Item No.", Item."No."); + ItemVarAttrValueMapping.SetRange("Variant Code", ItemVariant."Code"); + ItemVarAttrValueMapping.SetRange("Item Attribute ID", ProductOptions.Keys.Get(ProductOptionIndex)); + if ItemVarAttrValueMapping.FindFirst() then begin + if ItemAttributeValue.Get(ItemVarAttrValueMapping."Item Attribute ID", ItemVarAttrValueMapping."Item Attribute Value ID") then begin + ItemAttributeValue.CalcFields("Attribute Name"); + AssignProductOptionValues(TempShopifyVariant, ProductOptionIndex, ItemAttributeValue."Attribute Name", ItemAttributeValue.Value); + end else begin + SkippedRecord.LogSkippedRecord(Item.RecordId(), StrSubstNo(ItemVariantWithoutRequiredAttributeValueErr, Item."No."), Shop); + exit(false); + end; + end else begin + SkippedRecord.LogSkippedRecord(Item.RecordId(), StrSubstNo(ItemVariantWithoutRequiredAttributeErr, Item."No."), Shop); + exit(false); + end; + end else begin + ItemAttributeValueMapping.SetRange("Table ID", Database::Item); + ItemAttributeValueMapping.SetRange("No.", Item."No."); + ItemAttributeValueMapping.SetRange("Item Attribute ID", ProductOptions.Keys.Get(ProductOptionIndex)); + if ItemAttributeValueMapping.FindFirst() then begin + if ItemAttributeValue.Get(ItemAttributeValueMapping."Item Attribute ID", ItemAttributeValueMapping."Item Attribute Value ID") then begin + ItemAttributeValue.CalcFields("Attribute Name"); + AssignProductOptionValues(TempShopifyVariant, ProductOptionIndex, ItemAttributeValue."Attribute Name", ItemAttributeValue.Value); + end else begin + SkippedRecord.LogSkippedRecord(Item.RecordId(), StrSubstNo(ItemWithoutRequiredAttributeValueErr, Item."No."), Shop); + exit(false); + end; + end else begin + SkippedRecord.LogSkippedRecord(Item.RecordId(), StrSubstNo(ItemWithoutRequiredAttributeErr, Item."No."), Shop); + exit(false); + end; + end; + + exit(true); + end; + + local procedure CheckProductOptionsCombinationUnique(var TempShopifyVariant: Record "Shpfy Variant" temporary; ExistingProductOptionValues: Dictionary of [Text, Text]; Item: Record "Item"; ItemVariantCode: Code[10]): Boolean + var + ItemVariant: Record "Item Variant"; + CombinationKey: Text; + DuplicateItemCombinationErr: Label 'Item %1 cannot be added as a product variant because another variant already has the same option values.', Comment = '%1 = Item No.'; + DuplicateItemVarCombinationErr: Label 'Item %1 cannot be added as a product variant because another variant already has the same option values.', Comment = '%1 = Item No.'; + begin + CombinationKey := BuildCombinationKey( + TempShopifyVariant."Option 1 Name", TempShopifyVariant."Option 1 Value", + TempShopifyVariant."Option 2 Name", TempShopifyVariant."Option 2 Value", + TempShopifyVariant."Option 3 Name", TempShopifyVariant."Option 3 Value"); + + if ExistingProductOptionValues.ContainsKey(CombinationKey) then begin + if ItemVariant.Get(Item."No.", ItemVariantCode) then + SkippedRecord.LogSkippedRecord(ItemVariant.RecordId(), StrSubstNo(DuplicateItemVarCombinationErr, Item."No."), Shop) + else + SkippedRecord.LogSkippedRecord(Item.RecordId(), StrSubstNo(DuplicateItemCombinationErr, Item."No."), Shop); + exit(false); + end; + + exit(true); + end; + #endregion } diff --git a/src/Apps/W1/Shopify/App/src/Products/Enums/ShpfyInclInProductSync.Enum.al b/src/Apps/W1/Shopify/App/src/Products/Enums/ShpfyInclInProductSync.Enum.al new file mode 100644 index 0000000000..e9087b5b15 --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Products/Enums/ShpfyInclInProductSync.Enum.al @@ -0,0 +1,24 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Integration.Shopify; + +/// +/// Enum Shpfy Incl. in Product Sync (ID 30179). +/// +enum 30179 "Shpfy Incl. in Product Sync" +{ + Caption = 'Incl. in Product Sync'; + Extensible = false; + + value(0; " ") + { + Caption = ' ', Locked = true; + } + value(1; "As Option") + { + Caption = 'As Option'; + } +} diff --git a/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemAttributes.PageExt.al b/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemAttributes.PageExt.al new file mode 100644 index 0000000000..d6c0a33d1a --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemAttributes.PageExt.al @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Integration.Shopify; + +using Microsoft.Inventory.Item.Attribute; + +pageextension 30127 "Shpfy Item Attributes" extends "Item Attributes" +{ + layout + { + addlast(Control1) + { + field("Shpfy Incl. in Product Sync"; Rec."Shpfy Incl. in Product Sync") + { + ApplicationArea = All; + } + } + } +} diff --git a/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemCard.PageExt.al b/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemCard.PageExt.al index 495e86c364..94b8c2b7f3 100644 --- a/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemCard.PageExt.al +++ b/src/Apps/W1/Shopify/App/src/Products/Page Extensions/ShpfyItemCard.PageExt.al @@ -48,10 +48,16 @@ pageextension 30119 "Shpfy Item Card" extends "Item Card" trigger OnAction() var Shop: Record "Shpfy Shop"; + ProductExport: Codeunit "Shpfy Product Export"; SyncProducts: Codeunit "Shpfy Sync Products"; begin if SyncProducts.ConfirmAddItemToShopify(Rec, Shop) then begin + ProductExport.SetShop(Shop); + if not ProductExport.CheckItemAttributesCompatibleForProductOptions(Rec) then + exit; + SyncProducts.AddItemToShopify(Rec, Shop); + if Confirm(ViewInShopifyLbl) then Hyperlink(SyncProducts.GetProductUrl(Rec, Shop.Code)); CurrPage.Update(false); diff --git a/src/Apps/W1/Shopify/App/src/Products/Table Extensions/ShpfyItemAttribute.TableExt.al b/src/Apps/W1/Shopify/App/src/Products/Table Extensions/ShpfyItemAttribute.TableExt.al new file mode 100644 index 0000000000..077d0be0ca --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Products/Table Extensions/ShpfyItemAttribute.TableExt.al @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Integration.Shopify; + +using Microsoft.Inventory.Item.Attribute; + +tableextension 30112 "Shpfy Item Attribute" extends "Item Attribute" +{ + fields + { + field(30100; "Shpfy Incl. in Product Sync"; Enum "Shpfy Incl. in Product Sync") + { + Caption = 'Incl. in Product Sync'; + DataClassification = CustomerContent; + ToolTip = 'Specifies whether to include this item attribute in product synchronization to Shopify. Select "As Option" to export the attribute as a Shopify Product Option.'; + } + } +} diff --git a/src/Apps/W1/Shopify/Test/.resources/Products/VariantCreatedFromItemResponse.txt b/src/Apps/W1/Shopify/Test/.resources/Products/VariantCreatedFromItemResponse.txt new file mode 100644 index 0000000000..b40a1d8ff4 --- /dev/null +++ b/src/Apps/W1/Shopify/Test/.resources/Products/VariantCreatedFromItemResponse.txt @@ -0,0 +1 @@ +{"data":{"productVariantsBulkCreate":{"productVariants":[{"legacyResourceId":"55278024982862","createdAt":"2026-01-21T10:17:50Z","updatedAt":"2026-01-21T10:17:50Z"}],"userErrors":[]}},"extensions":{"cost":{"requestedQueryCost":10,"actualQueryCost":10,"throttleStatus":{"maximumAvailable":2000.0,"currentlyAvailable":1990,"restoreRate":100.0}}}} \ No newline at end of file diff --git a/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemVariantTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemVariantTest.Codeunit.al index ac3e8d7436..683babaf26 100644 --- a/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemVariantTest.Codeunit.al +++ b/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemVariantTest.Codeunit.al @@ -140,36 +140,6 @@ codeunit 139632 "Shpfy Create Item Variant Test" LibraryAssert.AreEqual(1, Options.Count(), 'Options not returned'); end; - [Test] - procedure UnitTestCreateVariantFromProductWithMultipleOptions() - var - Item: Record "Item"; - ShpfyProductInitTest: Codeunit "Shpfy Product Init Test"; - CreateItemAsVariant: Codeunit "Shpfy Create Item As Variant"; - CreateItemAsVariantSub: Codeunit "Shpfy CreateItemAsVariantSub"; - ProductId: BigInteger; - begin - // [SCENARIO] Create a variant from a product with multiple options - Initialize(); - - // [GIVEN] Item - Item := ShpfyProductInitTest.CreateItem(Shop."Item Templ. Code", Any.DecimalInRange(10, 100, 2), Any.DecimalInRange(100, 500, 2)); - // [GIVEN] Shopify product - ProductId := CreateShopifyProduct(Item.SystemId); - - // [GIVEN] Multiple options for the product in Shopify - CreateItemAsVariantSub.SetMultipleOptions(true); - - // [WHEN] Invoke ProductAPI.CheckProductAndShopSettings - BindSubscription(CreateItemAsVariantSub); - CreateItemAsVariant.SetParentProduct(ProductId); - asserterror CreateItemAsVariant.CheckProductAndShopSettings(); - UnbindSubscription(CreateItemAsVariantSub); - - // [THEN] Error is thrown - LibraryAssert.ExpectedError('The product has more than one option. Items cannot be added as variants to a product with multiple options.'); - end; - [Test] procedure UnitTestCreateVariantFromSameItem() var diff --git a/src/Apps/W1/Shopify/Test/Products/ShpfyItemAttrAsOptionTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Products/ShpfyItemAttrAsOptionTest.Codeunit.al new file mode 100644 index 0000000000..9d9914f82a --- /dev/null +++ b/src/Apps/W1/Shopify/Test/Products/ShpfyItemAttrAsOptionTest.Codeunit.al @@ -0,0 +1,730 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace Microsoft.Integration.Shopify.Test; + +using Microsoft.Integration.Shopify; +using Microsoft.Inventory.Item; +using Microsoft.Inventory.Item.Attribute; +using System.TestLibraries.Utilities; + +/// +/// Codeunit Shpfy Item Attr As Option Test (ID 139540). +/// Tests for 'Item Attributes As Shopify Product Options' functionality. +/// +codeunit 139540 "Shpfy Item Attr As Option Test" +{ + Subtype = Test; + TestType = IntegrationTest; + TestHttpRequestPolicy = BlockOutboundRequests; + TestPermissions = Disabled; + + var + Shop: Record "Shpfy Shop"; + Any: Codeunit Any; + LibraryVariableStorage: Codeunit "Library - Variable Storage"; + HttpHandlerParams: Codeunit "Library - Variable Storage"; + OutboundHttpRequests: Codeunit "Library - Variable Storage"; + LibraryAssert: Codeunit "Library Assert"; + LibraryInventory: Codeunit "Library - Inventory"; + InitializeTest: Codeunit "Shpfy Initialize Test"; + IsInitialized: Boolean; + UnexpectedAPICallsErr: Label 'More than expected API calls to Shopify detected.'; + + trigger OnRun() + begin + IsInitialized := false; + end; + + #region UoM as Variant validation Tests + [Test] + procedure UnitTestValidateUoMAsVariantWhenAsOptionAttributesExist() + var + ItemAttribute: Record "Item Attribute"; + FailureMessageErr: Label 'UoM as Variant is unavailable due to existing Item Attributes marked as “As Option” which are utilized for Shopify Product Options.'; + ExpectedErrorNotRaisedErr: Label 'Expected error was not raised.'; + begin + // [SCENARIO] Enabling 'UoM as Variant' fails when 'As Option' Item Attributes exist + + // [GIVEN] Shopify Shop is created, and UoM as Variant is false + Initialize(); + + // [GIVEN] Some Item Attributes are marked 'As Option' + CreateItemAttributeAsOption(ItemAttribute); + + // [WHEN] User tries to validate 'UoM as Variant' to true + asserterror Shop.Validate("UoM as Variant", true); + + // [THEN] Error is raised about unavailability + LibraryAssert.IsTrue(GetLastErrorText().Contains(FailureMessageErr), ExpectedErrorNotRaisedErr); + end; + #endregion + + #region No Variants, No As Option Attributes + [Test] + procedure UnitTestExportItemWithoutVariantsAndWithoutAsOptionAttributes() + var + Item: Record Item; + TempShopifyProduct: Record "Shpfy Product" temporary; + TempShopifyVariant: Record "Shpfy Variant" temporary; + TempTag: Record "Shpfy Tag" temporary; + CreateProduct: Codeunit "Shpfy Create Product"; + begin + // [SCENARIO] Exporting Item without variants and without 'As Option' attributes creates Shopify product variant with no options + + // [GIVEN] Shopify Shop is created + Initialize(); + CreateProduct.SetShop(Shop); + + // [GIVEN] Item is created without Item variants and without 'As Option' Item Attributes + CreateItem(Item); + + // [WHEN] Create Temp Product from Item + CreateProduct.CreateTempProduct(Item, TempShopifyProduct, TempShopifyVariant, TempTag); + + // [THEN] Product variant is created without Option Names and Option values + VerifyVariantHasNoOptions(TempShopifyVariant); + end; + + [Test] + [HandlerFunctions('HttpSubmitHandler_GetProductOptions')] + procedure UnitTestAddItemAsVariantToProductWithoutAsOptionAttributes() + var + Item: Record Item; + CreateItemAsVariant: Codeunit "Shpfy Create Item As Variant"; + ParentProductId: BigInteger; + ProdOptionNameTok: Label 'Variant', Locked = true; + begin + // [SCENARIO] Adding Item as variant to product without 'As Option' attributes creates variant with 'Variant' option + + // [GIVEN] Shopify Shop is created + Initialize(); + RegisterOutboundHttpRequests(); + HttpHandlerParams.Enqueue(ProdOptionNameTok); + + // [GIVEN] Product exists without As Option Attributes + ParentProductId := CreateShopifyProductWithoutAsOptionAttributes(); + + // [GIVEN] Item is created without Item variants and without 'As Option' Item Attributes + CreateItem(Item); + + // [WHEN] Add Item as Shopify Variant + CreateItemAsVariant.SetParentProduct(ParentProductId); + CreateItemAsVariant.CheckProductAndShopSettings(); + CreateItemAsVariant.CreateVariantFromItem(Item); + + // [THEN] New Variant is created with Option 1 Name 'Variant', Option 1 Value '' + VerifyVariantCreatedWithItemNo(ParentProductId, Item."No."); + end; + + #endregion + + #region No Variants, 2 As Option Attributes + [Test] + procedure UnitTestExportItemWithoutVariantsAndWith2AsOptionAttributes() + var + Item: Record Item; + TempShopifyProduct: Record "Shpfy Product" temporary; + TempShopifyVariant: Record "Shpfy Variant" temporary; + TempTag: Record "Shpfy Tag" temporary; + CreateProduct: Codeunit "Shpfy Create Product"; + begin + // [SCENARIO] Exporting Item without variants but with 2 'As Option' attributes creates variant with 2 options + + // [GIVEN] Shopify Shop is created + Initialize(); + CreateProduct.SetShop(Shop); + + // [GIVEN] Item is created without Item variants but with 2 'As Option' Item Attributes + Item := CreateItemWithAsOptionAttributes(2); + + // [WHEN] Create Temp Product from Item + CreateProduct.CreateTempProduct(Item, TempShopifyProduct, TempShopifyVariant, TempTag); + + // [THEN] Product variant is created with Item attributes as options + VerifyVariantHas2Options(TempShopifyVariant); + + // [THEN] Product should be marked as having variants + VerifyProductHasVariants(TempShopifyProduct); + end; + + [Test] + procedure UnitTestValidateItemAttributesForNewVariantMissingAttributes() + var + Item: Record Item; + ItemAttribute: Record "Item Attribute"; + TempShopifyVariant: Record "Shpfy Variant" temporary; + ProductExport: Codeunit "Shpfy Product Export"; + ValidationResult: Boolean; + ExpFailureMessageErr: Label 'cannot be added as a product variant because it does not have required attributes.'; + ParentProductId: BigInteger; + begin + // [SCENARIO] Adding Item as variant fails when Item is missing required 'As Option' attributes + + // [GIVEN] Shopify Shop is created + Initialize(); + ProductExport.SetShop(Shop); + + // [GIVEN] Item is created without Item variants but with 2 'As Option' Item Attributes + Item := CreateItemWithAsOptionAttributes(2); + + // [GIVEN] Product exists with 'As Option' Attributes + ParentProductId := CreateShopifyProductWithAsOptionAttributesAndValues(CopyStr(LibraryVariableStorage.PeekText(2), 1, 250), CopyStr(LibraryVariableStorage.PeekText(4), 1, 250), CopyStr(LibraryVariableStorage.PeekText(6), 1, 250), CopyStr(LibraryVariableStorage.PeekText(8), 1, 250)); + + // [GIVEN] Item is created without Item variants and without all required 'As Option' Item Attributes + LibraryInventory.CreateItemAttribute(ItemAttribute, ItemAttribute.Type::Text, ''); + Item := CreateItemWithSpecificAsOptionAttributes(LibraryVariableStorage.PeekInteger(1), LibraryVariableStorage.PeekInteger(3), CopyStr(LibraryVariableStorage.PeekText(4), 1, 250), ItemAttribute.ID, 0, GenerateRandomAttributeValue()); + + // [WHEN] Validate item attributes for new variant + TempShopifyVariant.Init(); + ValidationResult := ProductExport.ValidateItemAttributesAsProductOptionsForNewVariant(TempShopifyVariant, Item, '', ParentProductId); + + // [THEN] Returns false (variant should not be created) and skipped entry is logged + VerifyItemAttributesValidationForNewVariantFailed(ValidationResult); + VerifySkippedEntryExists(Item.RecordId, ExpFailureMessageErr); + end; + + [Test] + procedure UnitTestValidateItemAttributesForNewVariantDuplicateCombination() + var + Item: Record Item; + TempShopifyVariant: Record "Shpfy Variant" temporary; + ProductExport: Codeunit "Shpfy Product Export"; + ValidationResult: Boolean; + ExpFailureMessageErr: Label 'cannot be added as a product variant because another variant already has the same option values.'; + ParentProductId: BigInteger; + begin + // [SCENARIO] Adding Item as variant fails when option value combination already exists + + // [GIVEN] Shopify Shop is created + Initialize(); + ProductExport.SetShop(Shop); + + // [GIVEN] Item is created without Item variants but with 2 'As Option' Item Attributes + Item := CreateItemWithAsOptionAttributes(2); + + // [GIVEN] Product exists with 'As Option' Attributes + ParentProductId := CreateShopifyProductWithAsOptionAttributesAndValues(CopyStr(LibraryVariableStorage.PeekText(2), 1, 250), CopyStr(LibraryVariableStorage.PeekText(4), 1, 250), CopyStr(LibraryVariableStorage.PeekText(6), 1, 250), CopyStr(LibraryVariableStorage.PeekText(8), 1, 250)); + + // [GIVEN] Item is created with the same 'As Option' Item Attribute values as existing variant + Item := CreateItemWithSpecificAsOptionAttributes(LibraryVariableStorage.PeekInteger(1), LibraryVariableStorage.PeekInteger(3), CopyStr(LibraryVariableStorage.PeekText(4), 1, 250), LibraryVariableStorage.PeekInteger(5), LibraryVariableStorage.PeekInteger(7), CopyStr(LibraryVariableStorage.PeekText(8), 1, 250)); + + // [WHEN] Validate item attributes for new variant + TempShopifyVariant.Init(); + ValidationResult := ProductExport.ValidateItemAttributesAsProductOptionsForNewVariant(TempShopifyVariant, Item, '', ParentProductId); + + // [THEN] Returns false (variant should not be created) and skipped entry is logged + VerifyItemAttributesValidationForNewVariantFailed(ValidationResult); + VerifySkippedEntryExists(Item.RecordId, ExpFailureMessageErr); + end; + + [Test] + [HandlerFunctions('HttpSubmitHandler_GetProductOptions')] + procedure UnitTestAddItemAsVariantToProductWithAsOptionAttributes() + var + Item: Record Item; + ProductExport: Codeunit "Shpfy Product Export"; + CreateItemAsVariant: Codeunit "Shpfy Create Item As Variant"; + ParentProductId: BigInteger; + begin + // [SCENARIO] Item variant is successfully created for the product when Item attributes differs + + // [GIVEN] Shopify Shop is created + Initialize(); + ProductExport.SetShop(Shop); + RegisterOutboundHttpRequests(); + + // [GIVEN] Item is created without Item variants but with 2 'As Option' Item Attributes + Item := CreateItemWithAsOptionAttributes(2); + HttpHandlerParams.Enqueue(LibraryVariableStorage.PeekText(2)); + + // [GIVEN] Product exists with 'As Option' Attributes + ParentProductId := CreateShopifyProductWithAsOptionAttributesAndValues(CopyStr(LibraryVariableStorage.PeekText(2), 1, 250), CopyStr(LibraryVariableStorage.PeekText(4), 1, 250), CopyStr(LibraryVariableStorage.PeekText(6), 1, 250), CopyStr(LibraryVariableStorage.PeekText(8), 1, 250)); + + // [GIVEN] Item is created without Item variants and with all required 'As Option' Item Attributes + Item := CreateItemWithSpecificAsOptionAttributes(LibraryVariableStorage.PeekInteger(1), LibraryVariableStorage.PeekInteger(3), CopyStr(LibraryVariableStorage.PeekText(4), 1, 250), LibraryVariableStorage.PeekInteger(5), LibraryVariableStorage.PeekInteger(7), GenerateRandomAttributeValue()); + + // [WHEN] Add Item as Shopify Variant + CreateItemAsVariant.SetParentProduct(ParentProductId); + CreateItemAsVariant.CheckProductAndShopSettings(); + CreateItemAsVariant.CreateVariantFromItem(Item); + + // [THEN] New Variant is created with new combination of product options + VerifyVariantCreatedWithCorrectOptionValues(ParentProductId); + end; + + #endregion + + #region 2 Variants, No As Option Attributes + [Test] + procedure UnitTestExportItemWith2VariantsAndWithoutAsOptionAttributes() + var + Item: Record Item; + TempShopifyProduct: Record "Shpfy Product" temporary; + TempShopifyVariant: Record "Shpfy Variant" temporary; + TempTag: Record "Shpfy Tag" temporary; + CreateProduct: Codeunit "Shpfy Create Product"; + ExpOptionNameTok: Label 'Variant', Locked = true; + begin + // [SCENARIO] Exporting Item with 2 variants and without 'As Option' attributes creates 2 variants with 'Variant' option name + + // [GIVEN] Shopify Shop is created + Initialize(); + CreateProduct.SetShop(Shop); + + // [GIVEN] Item is created with 2 Item variants and without 'As Option' Item Attributes + CreateItemWithVariants(Item, 2); + + // [WHEN] Create Temp Product from Item + CreateProduct.CreateTempProduct(Item, TempShopifyProduct, TempShopifyVariant, TempTag); + + // [THEN] 2 Product variants are created (Option 1 Name for both 'Variant') + VerifyVariantsCreatedWithOptionName(TempShopifyVariant, 2, ExpOptionNameTok); + end; + + #endregion + + #region 2 Variants, 3 As Option Attributes + [Test] + procedure UnitTestExportItemWith2VariantsAnd3AsOptionAttributesDifferentCombinations() + var + Item: Record Item; + TempShopifyProduct: Record "Shpfy Product" temporary; + TempShopifyVariant: Record "Shpfy Variant" temporary; + TempTag: Record "Shpfy Tag" temporary; + CreateProduct: Codeunit "Shpfy Create Product"; + begin + // [SCENARIO] Exporting Item with 2 variants and 3 'As Option' attributes creates 2 variants with unique option combinations + + // [GIVEN] Shopify Shop is created + Initialize(); + CreateProduct.SetShop(Shop); + + // [GIVEN] Item is created with 2 Item variants and 3 'As Option' Item Attributes with different value combinations + Item := CreateItemWithVariantsAndAsOptionAttributes(2, 3, false); + + // [WHEN] Create Temp Product from Item + CreateProduct.CreateTempProduct(Item, TempShopifyProduct, TempShopifyVariant, TempTag); + + // [THEN] 2 Product variants are created with different Item attribute combinations + VerifyVariantsCreatedWith3Options(TempShopifyVariant, 2); + + // [THEN] Product should be marked as having variants + VerifyProductHasVariants(TempShopifyProduct); + end; + + [Test] + procedure UnitTestExportItemWith2VariantsAnd3AsOptionAttributesDuplicateCombinations() + var + Item: Record Item; + ProductExport: Codeunit "Shpfy Product Export"; + CompatibilityCheckResult: Boolean; + ExpFailureMessageErr: Label 'duplicate item variant attribute value combinations'; + begin + // [SCENARIO] Exporting Item with duplicate option value combinations fails with skipped entry + + // [GIVEN] Shopify Shop is created + Initialize(); + ProductExport.SetShop(Shop); + + // [GIVEN] Item is created with 2 Item variants and 3 'As Option' Item Attributes with duplicate value combinations + Item := CreateItemWithVariantsAndAsOptionAttributes(2, 3, true); + + // [WHEN] Check item attributes compatible for product options + CompatibilityCheckResult := ProductExport.CheckItemAttributesCompatibleForProductOptions(Item); + + // [THEN] Returns false (product should not be created) and skipped entry is logged + VerifyResultOfCompatibilityCheck(CompatibilityCheckResult); + VerifySkippedEntryExists(Item.RecordId, ExpFailureMessageErr); + end; + + [Test] + procedure UnitTestExportItemWithMoreThan3AsOptionAttributes() + var + Item: Record Item; + ProductExport: Codeunit "Shpfy Product Export"; + CompatibilityCheckResult: Boolean; + ExpFailureMessageErr: Label 'maximum of 3 product options'; + begin + // [SCENARIO] Exporting Item with more than 3 'As Option' attributes fails due to Shopify limit + + // [GIVEN] Shopify Shop is created + Initialize(); + ProductExport.SetShop(Shop); + + // [GIVEN] Item is created without Item variants but with 4 'As Option' Item Attributes (exceeds Shopify limit of 3) + Item := CreateItemWithAsOptionAttributes(4); + + // [WHEN] Check item attributes compatible for product options + CompatibilityCheckResult := ProductExport.CheckItemAttributesCompatibleForProductOptions(Item); + + // [THEN] Returns false and skipped entry is logged about too many attributes + VerifyResultOfCompatibilityCheck(CompatibilityCheckResult); + VerifySkippedEntryExists(Item.RecordId, ExpFailureMessageErr); + end; + #endregion + + #region Helper Procedures + local procedure Initialize() + var + CommunicationMgt: Codeunit "Shpfy Communication Mgt."; + LibraryRandom: Codeunit "Library - Random"; + AccessToken: SecretText; + begin + LibraryVariableStorage.Clear(); + OutboundHttpRequests.Clear(); + HttpHandlerParams.Clear(); + Any.SetDefaultSeed(); + if IsInitialized then + exit; + + Shop := InitializeTest.CreateShop(); + + // Disable Event Mocking + CommunicationMgt.SetTestInProgress(false); + //Register Shopify Access Token + AccessToken := LibraryRandom.RandText(20); + InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken); + Commit(); + IsInitialized := true; + end; + + local procedure CreateItemAttributeAsOption(var ItemAttribute: Record "Item Attribute") + begin + LibraryInventory.CreateItemAttribute(ItemAttribute, ItemAttribute.Type::Text, ''); + ItemAttribute."Shpfy Incl. in Product Sync" := "Shpfy Incl. in Product Sync"::"As Option"; + ItemAttribute.Modify(true); + end; + + local procedure GenerateRandomAttributeValue(): Text[250] + begin + exit(CopyStr(Any.AlphanumericText(50), 1, 250)); + end; + + local procedure CreateItem(var Item: Record Item) + var + ProductInitTest: Codeunit "Shpfy Product Init Test"; + ItemTemplateCode: Code[20]; + begin + ItemTemplateCode := Shop."Item Templ. Code"; + Item := ProductInitTest.CreateItem(ItemTemplateCode, Any.DecimalInRange(10, 100, 2), Any.DecimalInRange(100, 500, 2), false); + end; + + local procedure CreateItemWithVariants(var Item: Record Item; NumberOfVariants: Integer) + var + ItemVariant: Record "Item Variant"; + Index: Integer; + begin + CreateItem(Item); + + for Index := 1 to NumberOfVariants do begin + ItemVariant.Init(); + ItemVariant."Item No." := Item."No."; + ItemVariant.Code := CopyStr(Any.AlphabeticText(MaxStrLen(ItemVariant.Code)), 1, MaxStrLen(ItemVariant.Code)); + ItemVariant.Description := CopyStr(Any.AlphabeticText(50), 1, MaxStrLen(ItemVariant.Description)); + ItemVariant.Insert(true); + end; + end; + + local procedure CreateItemWithAsOptionAttributes(NumberOfAsOptionAttributes: Integer) Item: Record Item + var + ItemAttribute: Record "Item Attribute"; + ItemAttributeValue: Record "Item Attribute Value"; + Index: Integer; + begin + CreateItem(Item); + + for Index := 1 to NumberOfAsOptionAttributes do begin + CreateItemAttributeMappedToItem(Item, ItemAttribute, ItemAttributeValue); + + LibraryVariableStorage.Enqueue(ItemAttribute.ID); + LibraryVariableStorage.Enqueue(ItemAttribute.Name); + LibraryVariableStorage.Enqueue(ItemAttributeValue.ID); + LibraryVariableStorage.Enqueue(ItemAttributeValue.Value); + end; + end; + + local procedure CreateItemWithVariantsAndAsOptionAttributes(NumberOfVariants: Integer; NumberOfAsOptionAttributes: Integer; CreateDuplicateCombinations: Boolean) Item: Record Item + var + ItemAttribute: Record "Item Attribute"; + ItemAttributeValue: Record "Item Attribute Value"; + FirstAttributeValue: Record "Item Attribute Value"; + ItemVariant: Record "Item Variant"; + IsFirstVariant: Boolean; + Index: Integer; + begin + CreateItemWithVariants(Item, NumberOfVariants); + + for Index := 1 to NumberOfAsOptionAttributes do begin + CreateItemAttributeAsOption(ItemAttribute); + IsFirstVariant := true; + + ItemVariant.SetRange("Item No.", Item."No."); + if ItemVariant.FindSet() then + repeat + if CreateDuplicateCombinations then begin + if IsFirstVariant then begin + LibraryInventory.CreateItemAttributeValue(ItemAttributeValue, ItemAttribute.ID, GenerateRandomAttributeValue()); + FirstAttributeValue := ItemAttributeValue; + IsFirstVariant := false; + end else + ItemAttributeValue := FirstAttributeValue; + end else + LibraryInventory.CreateItemAttributeValue(ItemAttributeValue, ItemAttribute.ID, GenerateRandomAttributeValue()); + + LibraryInventory.CreateItemVariantAttributeValueMapping(Item."No.", ItemVariant.Code, ItemAttribute.ID, ItemAttributeValue.ID, Database::Item, Item."No."); + + LibraryVariableStorage.Enqueue(ItemAttribute.Name); + LibraryVariableStorage.Enqueue(ItemAttributeValue.Value); + until ItemVariant.Next() = 0; + + LibraryInventory.CreateItemAttributeValueMapping(Database::Item, Item."No.", ItemAttribute.ID, ItemAttributeValue.ID); + end; + end; + + local procedure CreateItemWithSpecificAsOptionAttributes(ItemAttributeID1: Integer; ItemAttributeValueID1: Integer; ItemAttributeValue1: Text[250]; ItemAttributeID2: Integer; ItemAttributeValueID2: Integer; ItemAttributeValue2: Text[250]) Item: Record Item + var + ItemAttributeValue: Record "Item Attribute Value"; + begin + CreateItem(Item); + + if not ItemAttributeValue.Get(ItemAttributeID1, ItemAttributeValueID1) then begin + LibraryInventory.CreateItemAttributeValue(ItemAttributeValue, ItemAttributeID1, ItemAttributeValue1); + ItemAttributeValueID1 := ItemAttributeValue.ID; + end; + + LibraryInventory.CreateItemAttributeValueMapping(Database::Item, Item."No.", ItemAttributeID1, ItemAttributeValueID1); + + if not ItemAttributeValue.Get(ItemAttributeID2, ItemAttributeValueID2) then begin + LibraryInventory.CreateItemAttributeValue(ItemAttributeValue, ItemAttributeID2, ItemAttributeValue2); + ItemAttributeValueID2 := ItemAttributeValue.ID; + end; + LibraryInventory.CreateItemAttributeValueMapping(Database::Item, Item."No.", ItemAttributeID2, ItemAttributeValueID2); + end; + + local procedure CreateShopifyProductWithoutAsOptionAttributes(): BigInteger + var + ShopifyProduct: Record "Shpfy Product"; + ShopifyVariant: Record "Shpfy Variant"; + begin + ShopifyProduct.Init(); + ShopifyProduct.Id := Any.IntegerInRange(10000, 99999); + ShopifyProduct."Shop Code" := Shop.Code; + ShopifyProduct.Title := CopyStr(Any.AlphabeticText(50), 1, MaxStrLen(ShopifyProduct.Title)); + ShopifyProduct.Insert(true); + + ShopifyVariant.Init(); + ShopifyVariant.Id := Any.IntegerInRange(10000, 99999); + ShopifyVariant."Product Id" := ShopifyProduct.Id; + ShopifyVariant."Shop Code" := Shop.Code; + ShopifyVariant."Option 1 Name" := 'Variant'; + ShopifyVariant."Option 1 Value" := 'Default'; + ShopifyVariant.Insert(true); + + exit(ShopifyProduct.Id); + end; + + local procedure CreateShopifyProductWithAsOptionAttributesAndValues(ItemAttributeName1: Text[250]; ItemAttributeValue1: Text[250]; ItemAttributeName2: Text[250]; ItemAttributeValue2: Text[250]): BigInteger + var + ShopifyProduct: Record "Shpfy Product"; + ShopifyVariant: Record "Shpfy Variant"; + begin + ShopifyProduct.Init(); + ShopifyProduct.Id := Any.IntegerInRange(10000, 99999); + ShopifyProduct."Shop Code" := Shop.Code; + ShopifyProduct.Title := CopyStr(Any.AlphabeticText(50), 1, MaxStrLen(ShopifyProduct.Title)); + ShopifyProduct."Has Variants" := true; + ShopifyProduct.Insert(true); + + ShopifyVariant.Init(); + ShopifyVariant.Id := Any.IntegerInRange(10000, 99999); + ShopifyVariant."Product Id" := ShopifyProduct.Id; + ShopifyVariant."Shop Code" := Shop.Code; + ShopifyVariant."Option 1 Name" := CopyStr(ItemAttributeName1, 1, MaxStrLen(ShopifyVariant."Option 1 Name")); + ShopifyVariant."Option 1 Value" := CopyStr(ItemAttributeValue1, 1, MaxStrLen(ShopifyVariant."Option 1 Value")); + ShopifyVariant."Option 2 Name" := CopyStr(ItemAttributeName2, 1, MaxStrLen(ShopifyVariant."Option 2 Name")); + ShopifyVariant."Option 2 Value" := CopyStr(ItemAttributeValue2, 1, MaxStrLen(ShopifyVariant."Option 2 Value")); + ShopifyVariant.Insert(true); + + exit(ShopifyProduct.Id); + end; + + local procedure VerifyVariantHasNoOptions(var TempShopifyVariant: Record "Shpfy Variant" temporary) + var + EmptyOptionName1Lbl: Label 'Option 1 Name should be empty'; + EmptyOptionName2Lbl: Label 'Option 2 Name should be empty'; + EmptyOptionValue1Lbl: Label 'Option 1 Value should be empty'; + EmptyOptionValue2Lbl: Label 'Option 2 Value should be empty'; + begin + LibraryAssert.RecordIsNotEmpty(TempShopifyVariant); + LibraryAssert.AreEqual('', TempShopifyVariant."Option 1 Name", EmptyOptionName1Lbl); + LibraryAssert.AreEqual('', TempShopifyVariant."Option 1 Value", EmptyOptionValue1Lbl); + LibraryAssert.AreEqual('', TempShopifyVariant."Option 2 Name", EmptyOptionName2Lbl); + LibraryAssert.AreEqual('', TempShopifyVariant."Option 2 Value", EmptyOptionValue2Lbl); + end; + + local procedure VerifyVariantCreatedWithItemNo(ParentProductId: BigInteger; ItemNo: Code[20]) + var + ShpfyVariant: Record "Shpfy Variant"; + Option1NameMismatchMsg: Label 'Option 1 Name should be ''Variant'''; + Option1ValueShouldBeItemNoMsg: Label 'Option 1 Value should be Item No.'; + begin + ShpfyVariant.SetRange("Product Id", ParentProductId); + ShpfyVariant.SetRange("Shop Code", Shop.Code); + ShpfyVariant.FindLast(); + LibraryAssert.AreEqual(HttpHandlerParams.PeekText(1), ShpfyVariant."Option 1 Name", Option1NameMismatchMsg); + LibraryAssert.AreEqual(ItemNo, ShpfyVariant."Option 1 Value", Option1ValueShouldBeItemNoMsg); + end; + + local procedure VerifyVariantCreatedWithCorrectOptionValues(ParentProductId: BigInteger) + var + ShpfyVariant: Record "Shpfy Variant"; + Option1NameMismatchMsg: Label 'Incorrect Option 1 Name'; + Option1ValueMismatchMsg: Label 'Incorrect Option 1 Value'; + Option2NameMismatchMsg: Label 'Incorrect Option 2 Name'; + Option2ValueMismatchMsg: Label 'Incorrect Option 2 Value'; + begin + ShpfyVariant.SetRange("Product Id", ParentProductId); + ShpfyVariant.SetRange("Shop Code", Shop.Code); + ShpfyVariant.FindLast(); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(2), ShpfyVariant."Option 1 Name", Option1NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(4), ShpfyVariant."Option 1 Value", Option1ValueMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(6), ShpfyVariant."Option 2 Name", Option2NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(8), ShpfyVariant."Option 2 Value", Option2ValueMismatchMsg); + end; + + local procedure VerifyVariantHas2Options(var TempShopifyVariant: Record "Shpfy Variant" temporary) + var + Option1NameMismatchMsg: Label 'Incorrect Option 1 Name'; + Option1ValueMismatchMsg: Label 'Incorrect Option 1 Value'; + Option2NameMismatchMsg: Label 'Incorrect Option 2 Name'; + Option2ValueMismatchMsg: Label 'Incorrect Option 2 Value'; + begin + LibraryAssert.RecordIsNotEmpty(TempShopifyVariant); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(2), TempShopifyVariant."Option 1 Name", Option1NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(4), TempShopifyVariant."Option 1 Value", Option1ValueMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(6), TempShopifyVariant."Option 2 Name", Option2NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(8), TempShopifyVariant."Option 2 Value", Option2ValueMismatchMsg); + end; + + local procedure VerifyProductHasVariants(var TempShopifyProduct: Record "Shpfy Product" temporary) + var + ProductHasVariantsMsg: Label 'Product should be marked as having variants'; + begin + LibraryAssert.IsTrue(TempShopifyProduct."Has Variants", ProductHasVariantsMsg); + end; + + local procedure VerifyItemAttributesValidationForNewVariantFailed(ValidationResult: Boolean) + var + IncorrectValidationResultMsg: Label 'Validation result was incorrect.'; + begin + LibraryAssert.IsFalse(ValidationResult, IncorrectValidationResultMsg); + end; + + local procedure VerifyVariantsCreatedWithOptionName(var TempShopifyVariant: Record "Shpfy Variant" temporary; ExpectedCount: Integer; ExpectedOption1Name: Text) + var + Option1ValueNotEmptyMsg: Label 'Option 1 Value should not be empty'; + Option1NameAssertionMsg: Label 'Incorrect Option 1 Name'; + IncorrectVariantCountMsg: Label 'Incorect count of variants has been be created'; + begin + LibraryAssert.AreEqual(ExpectedCount, TempShopifyVariant.Count(), IncorrectVariantCountMsg); + TempShopifyVariant.FindSet(); + repeat + LibraryAssert.AreEqual(ExpectedOption1Name, TempShopifyVariant."Option 1 Name", Option1NameAssertionMsg); + LibraryAssert.AreNotEqual('', TempShopifyVariant."Option 1 Value", Option1ValueNotEmptyMsg); + until TempShopifyVariant.Next() = 0; + end; + + local procedure VerifyVariantsCreatedWith3Options(var TempShopifyVariant: Record "Shpfy Variant" temporary; ExpectedCount: Integer) + var + Option1NameMismatchMsg: Label 'Option 1 Name has incorrect value.'; + Option1ValueMismatchMsg: Label 'Option 1 Value has incorrect value.'; + Option2NameMismatchMsg: Label 'Option 2 Name has incorrect value.'; + Option2ValueMismatchMsg: Label 'Option 2 Value has incorrect value.'; + Option3NameMismatchMsg: Label 'Option 3 Name has incorrect value.'; + Option3ValueMismatchMsg: Label 'Option 3 Value has incorrect value.'; + ItemVariantNumber: Integer; + begin + LibraryAssert.AreEqual(ExpectedCount, TempShopifyVariant.Count(), Format(ExpectedCount) + ' variants should be created'); + TempShopifyVariant.FindSet(); + repeat + ItemVariantNumber += 1; + if ItemVariantNumber = 1 then begin + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(1), TempShopifyVariant."Option 1 Name", Option1NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(2), TempShopifyVariant."Option 1 Value", Option1ValueMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(5), TempShopifyVariant."Option 2 Name", Option2NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(6), TempShopifyVariant."Option 2 Value", Option2ValueMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(9), TempShopifyVariant."Option 3 Name", Option3NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(10), TempShopifyVariant."Option 3 Value", Option3ValueMismatchMsg); + end; + if ItemVariantNumber = 2 then begin + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(3), TempShopifyVariant."Option 1 Name", Option1NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(4), TempShopifyVariant."Option 1 Value", Option1ValueMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(7), TempShopifyVariant."Option 2 Name", Option2NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(8), TempShopifyVariant."Option 2 Value", Option2ValueMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(11), TempShopifyVariant."Option 3 Name", Option3NameMismatchMsg); + LibraryAssert.AreEqual(LibraryVariableStorage.PeekText(12), TempShopifyVariant."Option 3 Value", Option3ValueMismatchMsg); + end; + until TempShopifyVariant.Next() = 0; + end; + + local procedure VerifyResultOfCompatibilityCheck(CompatibilityCheckResult: Boolean) + var + IncorrectResultMsg: Label 'Incorrect result of compatibility check.'; + begin + LibraryAssert.IsFalse(CompatibilityCheckResult, IncorrectResultMsg); + end; + + local procedure VerifySkippedEntryExists(ExpectedRecordId: RecordId; ExpFailureMessage: Text) + var + SkippedRecord: Record "Shpfy Skipped Record"; + begin + SkippedRecord.SetRange("Record ID", ExpectedRecordId); + SkippedRecord.SetFilter("Skipped Reason", '*' + ExpFailureMessage + '*'); + LibraryAssert.RecordIsNotEmpty(SkippedRecord); + end; + + local procedure CreateItemAttributeMappedToItem(var Item: Record Item; var ItemAttribute: Record "Item Attribute"; var ItemAttributeValue: Record "Item Attribute Value") + begin + CreateItemAttributeAsOption(ItemAttribute); + + LibraryInventory.CreateItemAttributeValue(ItemAttributeValue, ItemAttribute.ID, GenerateRandomAttributeValue()); + LibraryInventory.CreateItemAttributeValueMapping(Database::Item, Item."No.", ItemAttribute.ID, ItemAttributeValue.ID); + end; + + local procedure RegisterOutboundHttpRequests() + var + GqlProductOptionsLbl: Label 'GQL Product Options'; + VariantCreatedFromItemResponseLbl: Label 'GQL Prod. Variant Creation Response'; + begin + OutboundHttpRequests.Enqueue(GqlProductOptionsLbl); + OutboundHttpRequests.Enqueue(VariantCreatedFromItemResponseLbl); + end; + #endregion + + [HttpClientHandler] + internal procedure HttpSubmitHandler_GetProductOptions(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean + var + ResponseText: Text; + ProductOptionsResponseTok: Label 'Products/ProductOptionsResponse.txt', Locked = true; + VariantCreatedFromItemResponseTok: Label 'Products/VariantCreatedFromItemResponse.txt', Locked = true; + begin + if not InitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then + exit(true); + + case OutboundHttpRequests.Length() of + 2: + ResponseText := StrSubstNo(NavApp.GetResourceAsText(ProductOptionsResponseTok, TextEncoding::UTF8), HttpHandlerParams.PeekText(1)); + 1: + ResponseText := NavApp.GetResourceAsText(VariantCreatedFromItemResponseTok, TextEncoding::UTF8); + 0: + Error(UnexpectedAPICallsErr); + end; + + Response.Content.WriteFrom(ResponseText); + OutboundHttpRequests.DequeueText(); + exit(false); + end; +} \ No newline at end of file