diff --git a/Apps/W1/Shopify/app/src/Base/Pages/ShpfyShopCard.Page.al b/Apps/W1/Shopify/app/src/Base/Pages/ShpfyShopCard.Page.al
index 6c4dc6948d..bb7607e4c3 100644
--- a/Apps/W1/Shopify/app/src/Base/Pages/ShpfyShopCard.Page.al
+++ b/Apps/W1/Shopify/app/src/Base/Pages/ShpfyShopCard.Page.al
@@ -801,6 +801,19 @@ page 30101 "Shpfy Shop Card"
RunPageLink = "Shop Code" = field(Code);
ToolTip = 'View a list of Shopify Languages for the shop.';
}
+ action(SalesChannels)
+ {
+ ApplicationArea = All;
+ Caption = 'Sales Channels';
+ Image = List;
+ Promoted = true;
+ PromotedCategory = Category4;
+ PromotedIsBig = true;
+ PromotedOnly = true;
+ RunObject = Page "Shpfy Sales Channels";
+ RunPageLink = "Shop Code" = field(Code);
+ ToolTip = 'View a list of Shopify Sales Channels for the shop and choose ones used for new product publishing.';
+ }
}
area(Processing)
{
diff --git a/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLGetFulfillments.Codeunit.al b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLGetFulfillments.Codeunit.al
index bc893aa072..8fc5db971f 100644
--- a/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLGetFulfillments.Codeunit.al
+++ b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLGetFulfillments.Codeunit.al
@@ -1,3 +1,4 @@
+namespace Microsoft.Integration.Shopify;
///
/// Codeunit Shpfy GQL Get Fulfillments (ID 30317) implements Interface Shpfy IGraphQL.
///
diff --git a/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLGetNextSChannels.Codeunit.al b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLGetNextSChannels.Codeunit.al
new file mode 100644
index 0000000000..4da64241b8
--- /dev/null
+++ b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLGetNextSChannels.Codeunit.al
@@ -0,0 +1,26 @@
+namespace Microsoft.Integration.Shopify;
+///
+/// Codeunit Shpfy GQL Get Next S. Channels (ID 30375) implements Interface Shpfy IGraphQL.
+///
+codeunit 30384 "Shpfy GQL Get Next S. Channels" implements "Shpfy IGraphQL"
+{
+ Access = Internal;
+
+ ///
+ /// GetGraphQL.
+ ///
+ /// Return value of type Text.
+ internal procedure GetGraphQL(): Text
+ begin
+ exit('{"query" : "{publications(first: 25, after:\"{{After}}\", catalogType: APP) { pageInfo{ hasNextPage } edges { cursor node { id catalog { id ... on AppCatalog { apps(first: 1) { edges { node { id handle title } } } } } } } } }"}');
+ end;
+
+ ///
+ /// GetExpectedCost.
+ ///
+ /// Return value of type Integer.
+ internal procedure GetExpectedCost(): Integer
+ begin
+ exit(32);
+ end;
+}
diff --git a/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLGetSalesChannels.Codeunit.al b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLGetSalesChannels.Codeunit.al
new file mode 100644
index 0000000000..26e40de4c3
--- /dev/null
+++ b/Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLGetSalesChannels.Codeunit.al
@@ -0,0 +1,27 @@
+namespace Microsoft.Integration.Shopify;
+
+///
+/// Codeunit Shpfy GQL Get SalesChannels (ID 30371) implements Interface Shpfy IGraphQL.
+///
+codeunit 30371 "Shpfy GQL Get SalesChannels" implements "Shpfy IGraphQL"
+{
+ Access = Internal;
+
+ ///
+ /// GetGraphQL.
+ ///
+ /// Return value of type Text.
+ internal procedure GetGraphQL(): Text
+ begin
+ exit('{"query": "{publications(first: 25, catalogType: APP) { pageInfo{ hasNextPage } edges { cursor node { id catalog { id ... on AppCatalog { apps(first: 1) { edges { node { id handle title } } } } } } } } }"}');
+ end;
+
+ ///
+ /// GetExpectedCost.
+ ///
+ /// Return value of type Integer.
+ internal procedure GetExpectedCost(): Integer
+ begin
+ exit(22);
+ end;
+}
diff --git a/Apps/W1/Shopify/app/src/GraphQL/Enums/ShpfyGraphQLType.Enum.al b/Apps/W1/Shopify/app/src/GraphQL/Enums/ShpfyGraphQLType.Enum.al
index 6fbdb9d036..dd73957edf 100644
--- a/Apps/W1/Shopify/app/src/GraphQL/Enums/ShpfyGraphQLType.Enum.al
+++ b/Apps/W1/Shopify/app/src/GraphQL/Enums/ShpfyGraphQLType.Enum.al
@@ -490,6 +490,16 @@ enum 30111 "Shpfy GraphQL Type" implements "Shpfy IGraphQL"
Caption = 'Get Product Image';
Implementation = "Shpfy IGraphQL" = "Shpfy GQL GetProductImage";
}
+ value(101; GetSalesChannels)
+ {
+ Caption = 'Get Sales Channels';
+ Implementation = "Shpfy IGraphQL" = "Shpfy GQL Get SalesChannels";
+ }
+ value(102; GetNextSalesChannels)
+ {
+ Caption = 'Get Next Sales Channels';
+ Implementation = "Shpfy IGraphQL" = "Shpfy GQL Get Next S. Channels";
+ }
value(103; CustomerMetafieldIds)
{
Caption = 'Customer Metafield Ids';
diff --git a/Apps/W1/Shopify/app/src/PermissionSets/ShpfyEdit.PermissionSet.al b/Apps/W1/Shopify/app/src/PermissionSets/ShpfyEdit.PermissionSet.al
index b5b9defb15..dcf63a1780 100644
--- a/Apps/W1/Shopify/app/src/PermissionSets/ShpfyEdit.PermissionSet.al
+++ b/Apps/W1/Shopify/app/src/PermissionSets/ShpfyEdit.PermissionSet.al
@@ -58,6 +58,7 @@ permissionset 30102 "Shpfy - Edit"
tabledata "Shpfy Payout" = IMD,
tabledata "Shpfy Product" = IMD,
tabledata "Shpfy Registered Store New" = imd,
+ tabledata "Shpfy Sales Channel" = IMD,
tabledata "Shpfy Shipment Method Mapping" = IMD,
tabledata "Shpfy Shop" = IMD,
tabledata "Shpfy Shop Collection Map" = IMD,
diff --git a/Apps/W1/Shopify/app/src/PermissionSets/ShpfyRead.PermissionSet.al b/Apps/W1/Shopify/app/src/PermissionSets/ShpfyRead.PermissionSet.al
index a41a5b602e..d50c788a83 100644
--- a/Apps/W1/Shopify/app/src/PermissionSets/ShpfyRead.PermissionSet.al
+++ b/Apps/W1/Shopify/app/src/PermissionSets/ShpfyRead.PermissionSet.al
@@ -58,6 +58,7 @@ permissionset 30100 "Shpfy - Read"
tabledata "Shpfy Refund Line" = R,
tabledata "Shpfy Return Header" = R,
tabledata "Shpfy Return Line" = R,
+ tabledata "Shpfy Sales Channel" = R,
tabledata "Shpfy Shipment Method Mapping" = R,
tabledata "Shpfy Shop" = R,
tabledata "Shpfy Shop Collection Map" = R,
diff --git a/Apps/W1/Shopify/app/src/Products/Codeunits/ShpfyProductAPI.Codeunit.al b/Apps/W1/Shopify/app/src/Products/Codeunits/ShpfyProductAPI.Codeunit.al
index 8013899927..70a79ce555 100644
--- a/Apps/W1/Shopify/app/src/Products/Codeunits/ShpfyProductAPI.Codeunit.al
+++ b/Apps/W1/Shopify/app/src/Products/Codeunits/ShpfyProductAPI.Codeunit.al
@@ -66,6 +66,7 @@ codeunit 30176 "Shpfy Product API"
GraphQuery.Append(CommunicationMgt.EscapeGraphQLData(ShopifyProduct.Vendor));
GraphQuery.Append('\"');
end;
+
if ShopifyProduct."Has Variants" or (ShopifyVariant."UoM Option Id" > 0) then begin
GraphQuery.Append(', productOptions: [{name: \"');
GraphQuery.Append(CommunicationMgt.EscapeGraphQLData(ShopifyVariant."Option 1 Name"));
@@ -122,6 +123,8 @@ codeunit 30176 "Shpfy Product API"
VariantApi.AddProductVariant(ShopifyVariant);
end;
+ PublishProduct(NewShopifyProduct);
+
exit(NewShopifyProduct.Id);
end;
@@ -590,4 +593,65 @@ codeunit 30176 "Shpfy Product API"
foreach JOption in JOptions do
Options.Add(JsonHelper.GetValueAsText(JOption, 'id'), JsonHelper.GetValueAsText(JOption, 'name'));
end;
+
+ ///
+ /// Publish product to selected Shopify Sales Channels
+ ///
+ /// Shopify product to be published
+ internal procedure PublishProduct(ShopifyProduct: Record "Shpfy Product")
+ var
+ SalesChannel: Record "Shpfy Sales Channel";
+ GraphQuery: Text;
+ JResponse: JsonToken;
+ begin
+ if ShopifyProduct.Status <> Enum::"Shpfy Product Status"::Active then
+ exit;
+
+ if not FilterSalesChannelsToPublishTo(SalesChannel, ShopifyProduct."Shop Code") then
+ exit;
+
+ GraphQuery := CreateProductPublishGraphQuery(ShopifyProduct, SalesChannel);
+
+ JResponse := CommunicationMgt.ExecuteGraphQL(GraphQuery);
+ end;
+
+ local procedure FilterSalesChannelsToPublishTo(var SalesChannel: Record "Shpfy Sales Channel"; ShopCode: Code[20]): Boolean
+ var
+ SalesChannelAPI: Codeunit "Shpfy Sales Channel API";
+ begin
+ SalesChannel.SetRange("Shop Code", ShopCode);
+ if SalesChannel.IsEmpty() then
+ SalesChannelAPI.RetrieveSalesChannelsFromShopify(ShopCode);
+
+ SalesChannel.SetRange(SalesChannel."Use for publication", true);
+ if SalesChannel.IsEmpty() then begin
+ SalesChannel.SetRange("Use for publication");
+ SalesChannel.SetRange(SalesChannel.Default, true);
+ if SalesChannel.IsEmpty() then
+ exit(false);
+ end;
+
+ exit(true);
+ end;
+
+ local procedure CreateProductPublishGraphQuery(ShopifyProduct: Record "Shpfy Product"; var SalesChannel: Record "Shpfy Sales Channel"): Text
+ var
+ PublicationIds: TextBuilder;
+ PublicationIdTok: Label '{ publicationId: \"gid://shopify/Publication/%1\"},', Locked = true;
+ GraphQueryBuilder: TextBuilder;
+ begin
+ GraphQueryBuilder.Append('{"query":"mutation {publishablePublish(id: \"gid://shopify/Product/');
+ GraphQueryBuilder.Append(Format(ShopifyProduct.Id));
+ GraphQueryBuilder.Append('\" ');
+ GraphQueryBuilder.Append('input: [');
+ SalesChannel.FindSet();
+ repeat
+ PublicationIds.Append(StrSubstNo(PublicationIdTok, Format(SalesChannel.Id)));
+ until SalesChannel.Next() = 0;
+ GraphQueryBuilder.Append(PublicationIds.ToText().TrimEnd(','));
+ GraphQueryBuilder.Append('])');
+ GraphQueryBuilder.Append('{userErrors {field, message}}');
+ GraphQueryBuilder.Append('}"}');
+ exit(GraphQueryBuilder.ToText());
+ end;
}
\ No newline at end of file
diff --git a/Apps/W1/Shopify/app/src/Products/Codeunits/ShpfySalesChannelAPI.Codeunit.al b/Apps/W1/Shopify/app/src/Products/Codeunits/ShpfySalesChannelAPI.Codeunit.al
new file mode 100644
index 0000000000..6c8ba041dc
--- /dev/null
+++ b/Apps/W1/Shopify/app/src/Products/Codeunits/ShpfySalesChannelAPI.Codeunit.al
@@ -0,0 +1,98 @@
+namespace Microsoft.Integration.Shopify;
+
+///
+/// Codeunit Shpfy Sales Channel API (ID 30372).
+///
+codeunit 30372 "Shpfy Sales Channel API"
+{
+ Access = Internal;
+
+ var
+ JsonHelper: Codeunit "Shpfy Json Helper";
+ CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
+
+ ///
+ /// Retrieves the sales channels from Shopify and updates the table with the new sales channels.
+ ///
+ /// The code of the shop.
+ internal procedure RetrieveSalesChannelsFromShopify(ShopCode: Code[20])
+ var
+ GraphQLType: Enum "Shpfy GraphQL Type";
+ JResponse: JsonToken;
+ JPublications: JsonArray;
+ Cursor: Text;
+ Parameters: Dictionary of [Text, Text];
+ CurrentChannels: List of [BigInteger];
+ begin
+ CurrentChannels := CollectChannels(ShopCode);
+
+ CommunicationMgt.SetShop(ShopCode);
+ GraphQLType := GraphQLType::GetSalesChannels;
+
+ repeat
+ JResponse := CommunicationMgt.ExecuteGraphQL(GraphQLType, Parameters);
+ if JsonHelper.GetJsonArray(JResponse, JPublications, 'data.publications.edges') then begin
+ ExtractSalesChannels(JPublications, ShopCode, CurrentChannels, Cursor);
+ if Parameters.ContainsKey('After') then
+ Parameters.Set('After', Cursor)
+ else
+ Parameters.Add('After', Cursor);
+ GraphQLType := GraphQLType::GetNextSalesChannels;
+ end;
+ until not JsonHelper.GetValueAsBoolean(JResponse, 'data.publications.pageInfo.hasNextPage');
+
+ RemoveNotExistingChannels(CurrentChannels);
+ end;
+
+ local procedure CollectChannels(ShopCode: Code[20]): List of [BigInteger]
+ var
+ SalesChannel: Record "Shpfy Sales Channel";
+ Channels: List of [BigInteger];
+ begin
+ SalesChannel.SetRange("Shop Code", ShopCode);
+ if SalesChannel.FindSet() then
+ repeat
+ Channels.Add(SalesChannel.Id);
+ until SalesChannel.Next() = 0;
+ exit(Channels);
+ end;
+
+ local procedure RemoveNotExistingChannels(CurrentChannels: List of [BigInteger])
+ var
+ SalesChannel: Record "Shpfy Sales Channel";
+ ChannelId: BigInteger;
+ begin
+ foreach ChannelId in CurrentChannels do begin
+ SalesChannel.Get(ChannelId);
+ SalesChannel.Delete(true);
+ end;
+ end;
+
+ local procedure ExtractSalesChannels(JPublications: JsonArray; ShopCode: Code[20]; CurrentChannels: List of [BigInteger]; var Cursor: Text)
+ var
+ SalesChannel: Record "Shpfy Sales Channel";
+ JPublication: JsonToken;
+ ChannelId: BigInteger;
+ JCatalogEdges: JsonArray;
+ JCatalogEdge: JsonToken;
+ Handle: Text;
+ begin
+ foreach JPublication in JPublications do begin
+ Cursor := JsonHelper.GetValueAsText(JPublication, 'cursor');
+ ChannelId := CommunicationMgt.GetIdOfGId(JsonHelper.GetValueAsText(JPublication, '$.node.id'));
+ if not SalesChannel.Get(ChannelId) then begin
+ SalesChannel.Init();
+ SalesChannel.Validate(Id, ChannelId);
+ JCatalogEdges := JsonHelper.GetJsonArray(JPublication, '$.node.catalog.apps.edges');
+ JCatalogEdges.Get(0, JCatalogEdge);
+ SalesChannel.Validate(Name, JsonHelper.GetValueAsText(JCatalogEdge, '$.node.title'));
+ Handle := JsonHelper.GetValueAsText(JCatalogEdge, '$.node.handle');
+ if Handle = 'online_store' then
+ SalesChannel.Default := true;
+ SalesChannel."Shop Code" := ShopCode;
+ SalesChannel.Insert(true);
+ end else
+ CurrentChannels.Remove(ChannelId);
+ end;
+ end;
+}
diff --git a/Apps/W1/Shopify/app/src/Products/Pages/ShpfySalesChannels.Page.al b/Apps/W1/Shopify/app/src/Products/Pages/ShpfySalesChannels.Page.al
new file mode 100644
index 0000000000..84de596f41
--- /dev/null
+++ b/Apps/W1/Shopify/app/src/Products/Pages/ShpfySalesChannels.Page.al
@@ -0,0 +1,51 @@
+namespace Microsoft.Integration.Shopify;
+
+page 30167 "Shpfy Sales Channels"
+{
+ ApplicationArea = All;
+ Caption = 'Shopify Sales Channels';
+ PageType = List;
+ SourceTable = "Shpfy Sales Channel";
+ InsertAllowed = false;
+ DeleteAllowed = false;
+ UsageCategory = None;
+
+
+ layout
+ {
+ area(Content)
+ {
+ repeater(General)
+ {
+ field(Id; Rec.Id) { }
+ field(Name; Rec.Name) { }
+ field("Use for publication"; Rec."Use for publication") { }
+ field(Default; Rec.Default) { }
+ }
+ }
+ }
+
+ actions
+ {
+ area(Processing)
+ {
+ action(GetSalesChannels)
+ {
+ ApplicationArea = All;
+ Caption = 'Get Sales Channels';
+ Promoted = true;
+ PromotedOnly = true;
+ PromotedCategory = Process;
+ Image = UpdateDescription;
+ ToolTip = 'Retrieves the sales channels from Shopify.';
+
+ trigger OnAction()
+ var
+ ShpfySalesChannelAPI: Codeunit "Shpfy Sales Channel API";
+ begin
+ ShpfySalesChannelAPI.RetrieveSalesChannelsFromShopify(CopyStr(Rec.GetFilter("Shop Code"), 1, 20));
+ end;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Apps/W1/Shopify/app/src/Products/Tables/ShpfySalesChannel.Table.al b/Apps/W1/Shopify/app/src/Products/Tables/ShpfySalesChannel.Table.al
new file mode 100644
index 0000000000..5434a13d98
--- /dev/null
+++ b/Apps/W1/Shopify/app/src/Products/Tables/ShpfySalesChannel.Table.al
@@ -0,0 +1,50 @@
+namespace Microsoft.Integration.Shopify;
+
+///
+/// Table Shpfy Sales Channel (ID 30159).
+///
+table 30160 "Shpfy Sales Channel"
+{
+ Caption = 'Shopify Sales Channel';
+ DataClassification = CustomerContent;
+
+ fields
+ {
+ field(1; Id; BigInteger)
+ {
+ Caption = 'Id';
+ Editable = false;
+ ToolTip = 'Specifies the unique identifier of the sales channel.';
+ }
+ field(2; Name; Text[100])
+ {
+ Caption = 'Name';
+ Editable = false;
+ ToolTip = 'Specifies the name of the sales channel.';
+ }
+ field(3; "Shop Code"; Code[20])
+ {
+ Caption = 'Shop Code';
+ Editable = false;
+ ToolTip = 'Specifies the code of the shop.';
+ }
+ field(4; "Use for publication"; Boolean)
+ {
+ Caption = 'Use for publication';
+ ToolTip = 'Specifies if the sales channel is used for new products publication.';
+ }
+ field(5; Default; Boolean)
+ {
+ Caption = 'Default';
+ Editable = false;
+ ToolTip = 'Specifies if the sales channel is the default one. Used for new products publication if no other channel is selected';
+ }
+ }
+ keys
+ {
+ key(PK; Id)
+ {
+ Clustered = true;
+ }
+ }
+}
diff --git a/Apps/W1/Shopify/test/Products/ShpfySalesChannelHelper.Codeunit.al b/Apps/W1/Shopify/test/Products/ShpfySalesChannelHelper.Codeunit.al
new file mode 100644
index 0000000000..a1d6ddde0e
--- /dev/null
+++ b/Apps/W1/Shopify/test/Products/ShpfySalesChannelHelper.Codeunit.al
@@ -0,0 +1,16 @@
+///
+/// Codeunit Shpfy Sales Channel Helper (ID 139583).
+///
+codeunit 139699 "Shpfy Sales Channel Helper"
+{
+ internal procedure GetDefaultShopifySalesChannelResponse(OnlineStoreId: BigInteger; POSId: BigInteger): JsonArray
+ var
+ JPublications: JsonArray;
+ NodesTxt: Text;
+ ResponseTok: Label '[ { "node": { "id": "gid://shopify/Publication/%1", "catalog": {"apps": { "edges": [ { "node": { "handle": "online_store", "title": "Online Store" } } ] } } } }, { "node": { "id": "gid://shopify/Publication/%2", "catalog": { "apps": { "edges": [ { "node": {"handle": "pos", "title": "Point of Sale" } } ] } } } } ]', Locked = true;
+ begin
+ NodesTxt := StrSubstNo(ResponseTok, OnlineStoreId, POSId);
+ JPublications.ReadFrom(NodesTxt);
+ exit(JPublications);
+ end;
+}
\ No newline at end of file
diff --git a/Apps/W1/Shopify/test/Products/ShpfySalesChannelSubs.Codeunit.al b/Apps/W1/Shopify/test/Products/ShpfySalesChannelSubs.Codeunit.al
new file mode 100644
index 0000000000..ebcaa29b7e
--- /dev/null
+++ b/Apps/W1/Shopify/test/Products/ShpfySalesChannelSubs.Codeunit.al
@@ -0,0 +1,93 @@
+codeunit 139697 "Shpfy Sales Channel Subs."
+{
+ EventSubscriberInstance = Manual;
+
+ var
+ GraphQueryTxt: Text;
+ JEdges: JsonArray;
+
+ [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientSend', '', true, false)]
+ local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
+ begin
+ MakeResponse(HttpRequestMessage, HttpResponseMessage);
+ end;
+
+ [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnGetContent', '', true, false)]
+ local procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
+ begin
+ HttpResponseMessage.Content.ReadAs(Response);
+ end;
+
+ local procedure MakeResponse(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
+ var
+ GQLGetSalesChannels: Codeunit "Shpfy GQL Get SalesChannels";
+ Uri: Text;
+ GraphQlQuery: Text;
+ PublishProductTok: Label '{"query":"mutation {publishablePublish(id: \"gid://shopify/Product/', locked = true;
+ ProductCreateTok: Label '{"query":"mutation {productCreate(', locked = true;
+
+ GraphQLCmdTxt: Label '/graphql.json', Locked = true;
+ begin
+ case HttpRequestMessage.Method of
+ 'POST':
+ begin
+ Uri := HttpRequestMessage.GetRequestUri();
+ if Uri.EndsWith(GraphQLCmdTxt) then
+ if HttpRequestMessage.Content.ReadAs(GraphQlQuery) then
+ case true of
+ GraphQlQuery.Contains(PublishProductTok):
+ begin
+ HttpResponseMessage := GetEmptyPublishResponse();
+ GraphQueryTxt := GraphQlQuery;
+ end;
+ GraphQlQuery.Contains(ProductCreateTok):
+ HttpResponseMessage := GetCreateProductResponse();
+ GraphQlQuery = GQLGetSalesChannels.GetGraphQL():
+ HttpResponseMessage := GetSalesChannelsResponse();
+ end;
+ end;
+ end;
+ end;
+
+ local procedure GetEmptyPublishResponse(): HttpResponseMessage;
+ var
+ HttpResponseMessage: HttpResponseMessage;
+ BodyTxt: Text;
+ begin
+ BodyTxt := '{ "data": { "publishablePublish": { "userErrors": [] } }, "extensions": { "cost": { "requestedQueryCost": 10, "actualQueryCost": 10, "throttleStatus": { "maximumAvailable": 2000, "currentlyAvailable": 1990, "restoreRate": 100 } } } }';
+ HttpResponseMessage.Content.WriteFrom(BodyTxt);
+ exit(HttpResponseMessage);
+ end;
+
+ local procedure GetCreateProductResponse(): HttpResponseMessage
+ var
+ HttpResponseMessage: HttpResponseMessage;
+ BodyTxt: Text;
+ begin
+ BodyTxt := '{ "data": { "productCreate": { "product": { "legacyResourceId": "1234567890"} }}}';
+ HttpResponseMessage.Content.WriteFrom(BodyTxt);
+ exit(HttpResponseMessage);
+ end;
+
+ local procedure GetSalesChannelsResponse(): HttpResponseMessage
+ var
+ HttpResponseMessage: HttpResponseMessage;
+ BodyTxt: Text;
+ EdgesTxt: Text;
+ begin
+ JEdges.WriteTo(EdgesTxt);
+ BodyTxt := StrSubstNo('{ "data": { "publications": { "edges": %1 } }}', EdgesTxt);
+ HttpResponseMessage.Content.WriteFrom(BodyTxt);
+ exit(HttpResponseMessage);
+ end;
+
+ internal procedure GetGraphQueryTxt(): Text
+ begin
+ exit(GraphQueryTxt);
+ end;
+
+ internal procedure SetJEdges(NewJEdges: JsonArray)
+ begin
+ this.JEdges := NewJEdges;
+ end;
+}
\ No newline at end of file
diff --git a/Apps/W1/Shopify/test/Products/ShpfySalesChannelTest.Codeunit.al b/Apps/W1/Shopify/test/Products/ShpfySalesChannelTest.Codeunit.al
new file mode 100644
index 0000000000..e8790d848c
--- /dev/null
+++ b/Apps/W1/Shopify/test/Products/ShpfySalesChannelTest.Codeunit.al
@@ -0,0 +1,293 @@
+///
+/// Codeunit Shpfy Sales Channel Test (ID 139581).
+///
+codeunit 139698 "Shpfy Sales Channel Test"
+{
+ Subtype = Test;
+ TestPermissions = Disabled;
+
+ var
+ Shop: Record "Shpfy Shop";
+ Any: Codeunit Any;
+ LibraryAssert: Codeunit "Library Assert";
+ ShpfyInitializeTest: Codeunit "Shpfy Initialize Test";
+ SalesChannelHelper: Codeunit "Shpfy Sales Channel Helper";
+ IsInitialized: Boolean;
+
+ trigger OnRun()
+ begin
+ IsInitialized := false;
+ end;
+
+ [Test]
+ procedure UnitTestImportSalesChannelTest()
+ var
+ SalesChannel: Record "Shpfy Sales Channel";
+ JPublications: JsonArray;
+ begin
+ // [SCENARIO] Importing sales channel from Shopify to Business Central.
+ Initialize();
+
+ // [GIVEN] Shopify response with sales channel data.
+ JPublications := SalesChannelHelper.GetDefaultShopifySalesChannelResponse(Any.IntegerInRange(10000, 99999), Any.IntegerInRange(10000, 99999));
+
+ // [WHEN] Invoking the procedure: SalesChannelAPI.RetrieveSalesChannelsFromShopify
+ InvokeRetrieveSalesChannelsFromShopify(JPublications);
+
+ // [THEN] The sales channels are imported to Business Central.
+ SalesChannel.SetRange("Shop Code", Shop.Code);
+ LibraryAssert.IsFalse(SalesChannel.IsEmpty(), 'Sales Channel not created');
+ LibraryAssert.AreEqual(2, SalesChannel.Count(), 'Sales Channel count is not equal to 2');
+ SalesChannel.SetRange("Default", true);
+ LibraryAssert.IsFalse(SalesChannel.IsEmpty(), 'Default Sales Channel not created');
+ end;
+
+ [Test]
+ procedure UnitTestRemoveNotExistingChannelsTest()
+ var
+ SalesChannel: Record "Shpfy Sales Channel";
+ JPublications: JsonArray;
+ OnlineStoreId, POSId, AdditionalChannelId : BigInteger;
+ begin
+ // [SCENARIO] Removing not existing sales channels from Business Central.
+ Initialize();
+
+ // [GIVEN] Defult sales channels impported
+ OnlineStoreId := Any.IntegerInRange(10000, 99999);
+ POSId := Any.IntegerInRange(10000, 99999);
+ CreateDefaultSalesChannels(OnlineStoreId, POSId);
+ // [GIVEN] Additional sales channel
+ AdditionalChannelId := Any.IntegerInRange(10000, 99999);
+ CreateSalesChannel(Shop.Code, 'Additional Sales Channel', AdditionalChannelId, false);
+ // [GIVEN] Shopify response with default sales channel data.
+ JPublications := SalesChannelHelper.GetDefaultShopifySalesChannelResponse(OnlineStoreId, POSId);
+
+ // [WHEN] Invoking the procedure: SalesChannelAPI.InvokeRetreiveSalesChannelsFromShopify
+ InvokeRetrieveSalesChannelsFromShopify(JPublications);
+
+ // [THEN] The additional sales channel is removed from Business Central.
+ SalesChannel.SetRange("Shop Code", Shop.Code);
+ SalesChannel.SetRange("Id", AdditionalChannelId);
+ LibraryAssert.IsTrue(SalesChannel.IsEmpty(), 'Sales Channel not removed');
+ end;
+
+ [Test]
+ procedure UnitTestPublishProductWitArchivedStatusTest()
+ var
+ ShopifyProduct: Record "Shpfy Product";
+ ShopifyProductAPI: Codeunit "Shpfy Product API";
+ SalesChannelSubs: Codeunit "Shpfy Sales Channel Subs.";
+ GraphQueryTxt: Text;
+ begin
+ // [SCENARIO] Publishing not active product to Shopify Sales Channel.
+ Initialize();
+
+ // [GIVEN] Product with archived status.
+ CreateProductWithStatus(ShopifyProduct, Enum::"Shpfy Product Status"::Archived, Any.IntegerInRange(10000, 99999));
+
+ // [WHEN] Invoking the procedure: ShopifyProductAPI.PublishProduct(ShopifyProduct)
+ BindSubscription(SalesChannelSubs);
+ ShopifyProductAPI.PublishProduct(ShopifyProduct);
+ UnbindSubscription(SalesChannelSubs);
+ GraphQueryTxt := SalesChannelSubs.GetGraphQueryTxt();
+
+ // [THEN] Procedure exits without publishing the product.
+ LibraryAssert.AreEqual('', GraphQueryTxt, 'Query for publishing the product is generated');
+ end;
+
+ [Test]
+ procedure UnitTestPublishProductWithDraftStatusTest()
+ var
+ ShopifyProduct: Record "Shpfy Product";
+ ShopifyProductAPI: Codeunit "Shpfy Product API";
+ SalesChannelSubs: Codeunit "Shpfy Sales Channel Subs.";
+ GraphQueryTxt: Text;
+ begin
+ // [SCENARIO] Publishing draft product to Shopify Sales Channel.
+ Initialize();
+
+ // [GIVEN] Product with draft status.
+ CreateProductWithStatus(ShopifyProduct, Enum::"Shpfy Product Status"::Draft, Any.IntegerInRange(10000, 99999));
+ // [GIVEN] Default sales channels.
+ CreateDefaultSalesChannels(Any.IntegerInRange(10000, 99999), Any.IntegerInRange(10000, 99999));
+
+ // [WHEN] Invoking the procedure: ShopifyProductAPI.PublishProduct(ShopifyProduct)
+ BindSubscription(SalesChannelSubs);
+ ShopifyProductAPI.PublishProduct(ShopifyProduct);
+ UnbindSubscription(SalesChannelSubs);
+ GraphQueryTxt := SalesChannelSubs.GetGraphQueryTxt();
+
+ // [THEN] Procedure exits without publishing the product.
+ LibraryAssert.AreEqual('', GraphQueryTxt, 'Query for publishing the product is generated');
+ end;
+
+ [Test]
+ procedure UnitTestPublishProductToDefaultSalesChannelTest()
+ var
+ ShopifyProduct: Record "Shpfy Product";
+ ShopifyProductAPI: Codeunit "Shpfy Product API";
+ SalesChannelSubs: Codeunit "Shpfy Sales Channel Subs.";
+ OnlineShopId: BigInteger;
+ POSId: BigInteger;
+ ActualQuery: Text;
+ begin
+ // [SCENARIO] Publishing active product to Shopify Sales Channel.
+ Initialize();
+
+ // [GIVEN] Product with active status.
+ CreateProductWithStatus(ShopifyProduct, Enum::"Shpfy Product Status"::Active, Any.IntegerInRange(10000, 99999));
+ // [GIVEN] Default sales channels.
+ OnlineShopId := Any.IntegerInRange(10000, 99999);
+ POSId := OnlineShopId + 1;
+ CreateDefaultSalesChannels(OnlineShopId, POSId);
+
+ // [WHEN] Invoking the procedure: ShopifyProductAPI.PublishProduct(ShopifyProduct)
+ BindSubscription(SalesChannelSubs);
+ ShopifyProductAPI.PublishProduct(ShopifyProduct);
+ ActualQuery := SalesChannelSubs.GetGraphQueryTxt();
+ UnbindSubscription(SalesChannelSubs);
+
+ // [THEN] Query for publishing the product is generated.
+ LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo('id: \"gid://shopify/Product/%1\"', ShopifyProduct.Id)), 'Product Id is not in the query');
+ LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo('publicationId: \"gid://shopify/Publication/%1\"', OnlineShopId)), 'Publication Id is not in the query');
+ LibraryAssert.IsFalse(ActualQuery.Contains(StrSubstNo('publicationId: \"gid://shopify/Publication/%1\"', POSId)), 'Publication Id for POS is in the query');
+ end;
+
+ [Test]
+ procedure UnitTestPublishProductToMultipleSalesChannelsTest()
+ var
+ ShopifyProduct: Record "Shpfy Product";
+ ShopifyProductAPI: Codeunit "Shpfy Product API";
+ SalesChannelSubs: Codeunit "Shpfy Sales Channel Subs.";
+ OnlineShopId, POSId : BigInteger;
+ ActualQuery: Text;
+ begin
+ // [SCENARIO] Publishing active product to multiple Shopify Sales Channels.
+ Initialize();
+
+ // [GIVEN] Product with active status.
+ CreateProductWithStatus(ShopifyProduct, Enum::"Shpfy Product Status"::Active, Any.IntegerInRange(10000, 99999));
+ // [GIVEN] Default sales channels.
+ OnlineShopId := Any.IntegerInRange(10000, 99999);
+ POSId := OnlineShopId + 1;
+ CreateDefaultSalesChannels(OnlineShopId, POSId);
+ // [GIVEN] Online Shop used for publication
+ SetPublicationForSalesChannel(OnlineShopId);
+ // [GIVEN] POS used for publication
+ SetPublicationForSalesChannel(POSId);
+
+ // [WHEN] Invoking the procedure: ShopifyProductAPI.PublishProduct(ShopifyProduct)
+ BindSubscription(SalesChannelSubs);
+ ShopifyProductAPI.PublishProduct(ShopifyProduct);
+ ActualQuery := SalesChannelSubs.GetGraphQueryTxt();
+ UnbindSubscription(SalesChannelSubs);
+
+ // [THEN] Query for publishing the product to multiple sales channels is generated.
+ LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo('id: \"gid://shopify/Product/%1\"', ShopifyProduct.Id)), 'Product Id is not in the query');
+ LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo('publicationId: \"gid://shopify/Publication/%1\"', OnlineShopId)), 'Publication Id for Online Shop is not in the query');
+ LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo('publicationId: \"gid://shopify/Publication/%1\"', POSId)), 'Publication Id for POS is not in the query');
+ end;
+
+ [Test]
+ procedure UnitTestPublishProductOnCreateProductTest()
+ var
+ TempShopifyProduct: Record "Shpfy Product" temporary;
+ TempShopifyVariant: Record "Shpfy Variant" temporary;
+ ShopifyTag: Record "Shpfy Tag";
+ ShopifyProductAPI: Codeunit "Shpfy Product API";
+ SalesChannelSubs: Codeunit "Shpfy Sales Channel Subs.";
+ OnlineShopId, POSId : BigInteger;
+ ProductId: BigInteger;
+ ActualQuery: Text;
+ begin
+ // [SCENARIO] Publishing active product to Shopify Sales Channel on product creation.
+ Initialize();
+
+ // [GIVEN] Product with active status.
+ CreateProductWithStatus(TempShopifyProduct, Enum::"Shpfy Product Status"::Active, 0);
+ // [GIVEN] Shopify Variant
+ CreateShopifyVariant(TempShopifyProduct, TempShopifyVariant, 0);
+ // [GIVEN] Default sales channels.
+ OnlineShopId := Any.IntegerInRange(10000, 99999);
+ POSId := OnlineShopId + 1;
+ CreateDefaultSalesChannels(OnlineShopId, POSId);
+
+ // [WHEN] Invoke Product API
+ BindSubscription(SalesChannelSubs);
+ ProductId := ShopifyProductAPI.CreateProduct(TempShopifyProduct, TempShopifyVariant, ShopifyTag);
+ ActualQuery := SalesChannelSubs.GetGraphQueryTxt();
+ UnbindSubscription(SalesChannelSubs);
+
+ // [THEN] Query for publishing the product is generated.
+ LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo('id: \"gid://shopify/Product/%1\"', ProductId)), 'Product Id is not in the query');
+ LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo('publicationId: \"gid://shopify/Publication/%1\"', OnlineShopId)), 'Publication Id for Online Shop is not in the query');
+ end;
+
+ local procedure Initialize()
+ begin
+ Any.SetDefaultSeed();
+ if IsInitialized then
+ exit;
+ Shop := ShpfyInitializeTest.CreateShop();
+ IsInitialized := true;
+ Commit();
+ end;
+
+ local procedure CreateSalesChannel(ShopCode: Code[20]; ChannelName: Text; ChannelId: BigInteger; IsDefault: Boolean)
+ var
+ SalesChannel: Record "Shpfy Sales Channel";
+ begin
+ SalesChannel.Init();
+ SalesChannel.Id := ChannelId;
+ SalesChannel."Shop Code" := ShopCode;
+ SalesChannel.Name := ChannelName;
+ SalesChannel.Default := IsDefault;
+ SalesChannel.Insert(true);
+ end;
+
+ local procedure CreateDefaultSalesChannels(OnlineStoreId: BigInteger; POSId: BigInteger)
+ var
+ SalesChannel: Record "Shpfy Sales Channel";
+ begin
+ SalesChannel.DeleteAll(false);
+ CreateSalesChannel(Shop.Code, 'Online Store', OnlineStoreId, true);
+ CreateSalesChannel(Shop.Code, 'Point of Sale', POSId, false);
+ end;
+
+ local procedure CreateProductWithStatus(var ShopifyProduct: Record "Shpfy Product"; ShpfyProductStatus: Enum Microsoft.Integration.Shopify."Shpfy Product Status"; Id: BigInteger)
+ begin
+ ShopifyProduct.Init();
+ ShopifyProduct.Id := Id;
+ ShopifyProduct."Shop Code" := Shop.Code;
+ ShopifyProduct.Status := ShpfyProductStatus;
+ ShopifyProduct.Insert(true);
+ end;
+
+ local procedure SetPublicationForSalesChannel(SalesChannelId: BigInteger)
+ var
+ SalesChannel: Record "Shpfy Sales Channel";
+ begin
+ SalesChannel.Get(SalesChannelId);
+ SalesChannel."Use for publication" := true;
+ SalesChannel.Modify(false);
+ end;
+
+ local procedure CreateShopifyVariant(ShopifyProduct: Record "Shpfy Product"; var ShpfyVariant: Record "Shpfy Variant"; Id: BigInteger)
+ begin
+ ShpfyVariant.Init();
+ ShpfyVariant.Id := Id;
+ ShpfyVariant."Product Id" := ShopifyProduct.Id;
+ ShpfyVariant.Insert(false);
+ end;
+
+ local procedure InvokeRetrieveSalesChannelsFromShopify(var JPublications: JsonArray)
+ var
+ SalesChannelAPI: Codeunit "Shpfy Sales Channel API";
+ SalesChannelSubs: Codeunit "Shpfy Sales Channel Subs.";
+ begin
+ BindSubscription(SalesChannelSubs);
+ SalesChannelSubs.SetJEdges(JPublications);
+ SalesChannelAPI.RetrieveSalesChannelsFromShopify(Shop.Code);
+ UnbindSubscription(SalesChannelSubs);
+ end;
+}
\ No newline at end of file