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