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