Skip to content

Commit

Permalink
[Shopify] Sales Channels publishing for new products (#27540)
Browse files Browse the repository at this point in the history
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 <[email protected]>
Co-authored-by: Jesper Schulz-Wedde <[email protected]>
Co-authored-by: Onat Buyukakkus <[email protected]>
  • Loading branch information
4 people authored Dec 6, 2024
1 parent 83d72b0 commit 07ec75f
Show file tree
Hide file tree
Showing 14 changed files with 744 additions and 0 deletions.
13 changes: 13 additions & 0 deletions Apps/W1/Shopify/app/src/Base/Pages/ShpfyShopCard.Page.al
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
namespace Microsoft.Integration.Shopify;
/// <summary>
/// Codeunit Shpfy GQL Get Fulfillments (ID 30317) implements Interface Shpfy IGraphQL.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Microsoft.Integration.Shopify;
/// <summary>
/// Codeunit Shpfy GQL Get Next S. Channels (ID 30375) implements Interface Shpfy IGraphQL.
/// </summary>
codeunit 30384 "Shpfy GQL Get Next S. Channels" implements "Shpfy IGraphQL"
{
Access = Internal;

/// <summary>
/// GetGraphQL.
/// </summary>
/// <returns>Return value of type Text.</returns>
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;

/// <summary>
/// GetExpectedCost.
/// </summary>
/// <returns>Return value of type Integer.</returns>
internal procedure GetExpectedCost(): Integer
begin
exit(32);
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Microsoft.Integration.Shopify;

/// <summary>
/// Codeunit Shpfy GQL Get SalesChannels (ID 30371) implements Interface Shpfy IGraphQL.
/// </summary>
codeunit 30371 "Shpfy GQL Get SalesChannels" implements "Shpfy IGraphQL"
{
Access = Internal;

/// <summary>
/// GetGraphQL.
/// </summary>
/// <returns>Return value of type Text.</returns>
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;

/// <summary>
/// GetExpectedCost.
/// </summary>
/// <returns>Return value of type Integer.</returns>
internal procedure GetExpectedCost(): Integer
begin
exit(22);
end;
}
10 changes: 10 additions & 0 deletions Apps/W1/Shopify/app/src/GraphQL/Enums/ShpfyGraphQLType.Enum.al
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down Expand Up @@ -122,6 +123,8 @@ codeunit 30176 "Shpfy Product API"
VariantApi.AddProductVariant(ShopifyVariant);
end;

PublishProduct(NewShopifyProduct);

exit(NewShopifyProduct.Id);
end;

Expand Down Expand Up @@ -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;

/// <summary>
/// Publish product to selected Shopify Sales Channels
/// </summary>
/// <param name="ShopifyProduct">Shopify product to be published</param>
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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
namespace Microsoft.Integration.Shopify;

/// <summary>
/// Codeunit Shpfy Sales Channel API (ID 30372).
/// </summary>
codeunit 30372 "Shpfy Sales Channel API"
{
Access = Internal;

var
JsonHelper: Codeunit "Shpfy Json Helper";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";

/// <summary>
/// Retrieves the sales channels from Shopify and updates the table with the new sales channels.
/// </summary>
/// <param name="ShopCode">The code of the shop.</param>
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;
}
51 changes: 51 additions & 0 deletions Apps/W1/Shopify/app/src/Products/Pages/ShpfySalesChannels.Page.al
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
50 changes: 50 additions & 0 deletions Apps/W1/Shopify/app/src/Products/Tables/ShpfySalesChannel.Table.al
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace Microsoft.Integration.Shopify;

/// <summary>
/// Table Shpfy Sales Channel (ID 30159).
/// </summary>
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;
}
}
}
Loading

0 comments on commit 07ec75f

Please sign in to comment.