From 07ec75fbd436dd3e7cc2a2374dea8361e40fb257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tine=20Stari=C4=8D?= <42935028+tinestaric@users.noreply.github.com> Date: Fri, 6 Dec 2024 09:55:19 +0000 Subject: [PATCH] [Shopify] Sales Channels publishing for new products (#27540) This pull request does not have a related issue as it's part of delivery for development agreed directly with @AndreiPanko Fixes #26819 ### Sales Channels publishing for new products This PR contains new functionality where user can choose Sales Channels which will new products be published to on creation from BC. Solution contains new page and table - Shopify Sales Channels available from Shopify Shop. On page user can import sales channels available for shop and choose to which new products should be published to. If user does not choose any of the channels, then channel "Online Shop" is being used by default. In case user did not import Sales Channels before running create product sales channels are pulled from Shopify on before publishing and default channel is used. Pulled sales channels are updated in BC - in case the channel was removed in Shopify its removed from list in BC. ### Modified/new objects - New GQL query codeunit was created to import the Sales Channels. - New GQL Query Enum Value - New codeunit Shpfy Sales Channel API to handle sales channels import - Removed detracted published = true from Shpfy Product API, added procedures to handle new way of publishing new products to sales channels - Sales Channels page and table - New action on Shopify Shop Card page - Sales Channels Fixes [AB#556526](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/556526), [AB#550319](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/550319) --------- Co-authored-by: Piotr Michalak Co-authored-by: Jesper Schulz-Wedde Co-authored-by: Onat Buyukakkus <55088871+onbuyuka@users.noreply.github.com> --- .../app/src/Base/Pages/ShpfyShopCard.Page.al | 13 + .../ShpfyGQLGetFulfillments.Codeunit.al | 1 + .../ShpfyGQLGetNextSChannels.Codeunit.al | 26 ++ .../ShpfyGQLGetSalesChannels.Codeunit.al | 27 ++ .../GraphQL/Enums/ShpfyGraphQLType.Enum.al | 10 + .../PermissionSets/ShpfyEdit.PermissionSet.al | 1 + .../PermissionSets/ShpfyRead.PermissionSet.al | 1 + .../Codeunits/ShpfyProductAPI.Codeunit.al | 64 ++++ .../ShpfySalesChannelAPI.Codeunit.al | 98 ++++++ .../Products/Pages/ShpfySalesChannels.Page.al | 51 +++ .../Tables/ShpfySalesChannel.Table.al | 50 +++ .../ShpfySalesChannelHelper.Codeunit.al | 16 + .../ShpfySalesChannelSubs.Codeunit.al | 93 ++++++ .../ShpfySalesChannelTest.Codeunit.al | 293 ++++++++++++++++++ 14 files changed, 744 insertions(+) create mode 100644 Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLGetNextSChannels.Codeunit.al create mode 100644 Apps/W1/Shopify/app/src/GraphQL/Codeunits/ShpfyGQLGetSalesChannels.Codeunit.al create mode 100644 Apps/W1/Shopify/app/src/Products/Codeunits/ShpfySalesChannelAPI.Codeunit.al create mode 100644 Apps/W1/Shopify/app/src/Products/Pages/ShpfySalesChannels.Page.al create mode 100644 Apps/W1/Shopify/app/src/Products/Tables/ShpfySalesChannel.Table.al create mode 100644 Apps/W1/Shopify/test/Products/ShpfySalesChannelHelper.Codeunit.al create mode 100644 Apps/W1/Shopify/test/Products/ShpfySalesChannelSubs.Codeunit.al create mode 100644 Apps/W1/Shopify/test/Products/ShpfySalesChannelTest.Codeunit.al 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