From 205469a6c7f5559344898d69b6da02315ec582bd Mon Sep 17 00:00:00 2001
From: Onat Buyukakkus <55088871+onbuyuka@users.noreply.github.com>
Date: Wed, 21 Jan 2026 21:01:04 +0100
Subject: [PATCH] Shopify API 2026-01 uptake
---
.../ShpfyCommunicationMgt.Codeunit.al | 2 +-
.../ShpfyBulkOperationAPI.Codeunit.al | 19 -
.../ShpfyBulkOperationMgt.Codeunit.al | 23 +-
.../ShpfyGQLBulkOperations.Codeunit.al | 29 --
.../ShpfyGQLModifyInventory.Codeunit.al | 2 +-
.../Codeunits/ShpfyGQLNextPayouts.Codeunit.al | 2 +-
.../ShpfyGQLNextReturnLines.Codeunit.al | 2 +-
.../Codeunits/ShpfyGQLPayouts.Codeunit.al | 2 +-
.../Codeunits/ShpfyGQLReturnLines.Codeunit.al | 2 +-
.../Codeunits/ShpfyGQLVariantById.Codeunit.al | 2 +-
.../GraphQL/Enums/ShpfyGraphQLType.Enum.al | 5 -
.../Codeunits/ShpfyInventoryAPI.Codeunit.al | 83 ++++-
.../ShpfyMtfldTypeArticleRef.Codeunit.al | 34 ++
.../Enums/ShpfyMetafieldType.Enum.al | 6 +
.../Codeunits/ShpfyReturnsAPI.Codeunit.al | 6 +-
.../Pages/ShpfyReturnLines.Page.al | 11 +
.../Tables/ShpfyReturnLine.Table.al | 22 ++
.../Codeunits/ShpfyPaymentsAPI.Codeunit.al | 3 +-
.../src/Payments/Tables/ShpfyPayout.Table.al | 5 +
.../ShpfyObjects.PermissionSet.al | 1 -
.../Codeunits/ShpfyCreateProduct.Codeunit.al | 4 -
.../Codeunits/ShpfyProductExport.Codeunit.al | 3 -
.../Codeunits/ShpfyVariantAPI.Codeunit.al | 43 ++-
.../src/Products/Pages/ShpfyVariants.Page.al | 6 +
.../src/Products/Tables/ShpfyVariant.Table.al | 10 +
.../CurrentBulkOperationCompletedResult.txt | 1 -
.../CurrentBulkOperationRunningResult.txt | 1 -
.../ShpfyBulkOpSubscriber.Codeunit.al | 30 --
.../ShpfyInventoryExportTest.Codeunit.al | 347 ++++++++++++++++++
.../ShpfyInventoryRetryScenario.Enum.al | 28 ++
.../ShpfyInventorySubscriber.Codeunit.al | 103 ++++++
.../ShpfyOrderRefundsHelper.Codeunit.al | 41 ++-
.../Payments/ShpfyPaymentsTest.Codeunit.al | 56 +++
.../ShpfyVariantBatchingTest.Codeunit.al | 196 ++++++++++
34 files changed, 972 insertions(+), 158 deletions(-)
delete mode 100644 src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLBulkOperations.Codeunit.al
create mode 100644 src/Apps/W1/Shopify/App/src/Metafields/Codeunits/IMetafieldType/ShpfyMtfldTypeArticleRef.Codeunit.al
delete mode 100644 src/Apps/W1/Shopify/Test/.resources/Bulk Operations/CurrentBulkOperationCompletedResult.txt
delete mode 100644 src/Apps/W1/Shopify/Test/.resources/Bulk Operations/CurrentBulkOperationRunningResult.txt
create mode 100644 src/Apps/W1/Shopify/Test/Inventory/ShpfyInventoryExportTest.Codeunit.al
create mode 100644 src/Apps/W1/Shopify/Test/Inventory/ShpfyInventoryRetryScenario.Enum.al
create mode 100644 src/Apps/W1/Shopify/Test/Inventory/ShpfyInventorySubscriber.Codeunit.al
create mode 100644 src/Apps/W1/Shopify/Test/Products/ShpfyVariantBatchingTest.Codeunit.al
diff --git a/src/Apps/W1/Shopify/App/src/Base/Codeunits/ShpfyCommunicationMgt.Codeunit.al b/src/Apps/W1/Shopify/App/src/Base/Codeunits/ShpfyCommunicationMgt.Codeunit.al
index 62f4019655..1b30497ac6 100644
--- a/src/Apps/W1/Shopify/App/src/Base/Codeunits/ShpfyCommunicationMgt.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/Base/Codeunits/ShpfyCommunicationMgt.Codeunit.al
@@ -22,7 +22,7 @@ codeunit 30103 "Shpfy Communication Mgt."
CommunicationEvents: Codeunit "Shpfy Communication Events";
GraphQLQueries: Codeunit "Shpfy GraphQL Queries";
NextExecutionTime: DateTime;
- VersionTok: Label '2025-07', Locked = true;
+ VersionTok: Label '2026-01', Locked = true;
OutgoingRequestsNotEnabledConfirmLbl: Label 'Importing data to your Shopify shop is not enabled, do you want to go to shop card to enable?';
OutgoingRequestsNotEnabledErr: Label 'Importing data to your Shopify shop is not enabled, navigate to shop card to enable.';
IsTestInProgress: Boolean;
diff --git a/src/Apps/W1/Shopify/App/src/Bulk Operations/Codeunits/ShpfyBulkOperationAPI.Codeunit.al b/src/Apps/W1/Shopify/App/src/Bulk Operations/Codeunits/ShpfyBulkOperationAPI.Codeunit.al
index f860462b81..0e2ed89009 100644
--- a/src/Apps/W1/Shopify/App/src/Bulk Operations/Codeunits/ShpfyBulkOperationAPI.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/Bulk Operations/Codeunits/ShpfyBulkOperationAPI.Codeunit.al
@@ -22,25 +22,6 @@ codeunit 30278 "Shpfy Bulk Operation API"
CommunicationMgt.SetShop(Shop);
end;
- internal procedure GetCurrentBulkRequest(var BulkOperationId: BigInteger; var Status: Enum "Shpfy Bulk Operation Status"; var ErrorCode: Text; var CompletedAt: DateTime; var Url: Text; var PartialDataUrl: Text)
- var
- JsonHelper: Codeunit "Shpfy Json Helper";
- GraphQLType: Enum "Shpfy GraphQL Type";
- Parameters: Dictionary of [Text, Text];
- JResponse: JsonToken;
- JBulkOperation: JsonObject;
- begin
- JResponse := CommunicationMgt.ExecuteGraphQL(GraphQLType::GetCurrentBulkOperation, Parameters);
- if JsonHelper.GetJsonObject(JResponse, JBulkOperation, 'data.currentBulkOperation') then begin
- BulkOperationId := CommunicationMgt.GetIdOfGId(JsonHelper.GetValueAsText(JBulkOperation, 'id'));
- Status := ConvertToBulkOperationStatus(JsonHelper.GetValueAsText(JBulkOperation, 'status'));
- ErrorCode := JsonHelper.GetValueAsText(JBulkOperation, 'errorCode');
- CompletedAt := JsonHelper.GetValueAsDateTime(JBulkOperation, 'completedAt');
- Url := JsonHelper.GetValueAsText(JBulkOperation, 'url');
- PartialDataUrl := JsonHelper.GetValueAsText(JBulkOperation, 'partialDataUrl');
- end;
- end;
-
internal procedure GetBulkRequest(BulkOperationId: BigInteger; var Status: Enum "Shpfy Bulk Operation Status"; var ErrorCode: Text; var CompletedAt: DateTime; var Url: Text; var PartialDataUrl: Text)
var
JsonHelper: Codeunit "Shpfy Json Helper";
diff --git a/src/Apps/W1/Shopify/App/src/Bulk Operations/Codeunits/ShpfyBulkOperationMgt.Codeunit.al b/src/Apps/W1/Shopify/App/src/Bulk Operations/Codeunits/ShpfyBulkOperationMgt.Codeunit.al
index 3efdaf0142..5b93622889 100644
--- a/src/Apps/W1/Shopify/App/src/Bulk Operations/Codeunits/ShpfyBulkOperationMgt.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/Bulk Operations/Codeunits/ShpfyBulkOperationMgt.Codeunit.al
@@ -11,8 +11,6 @@ codeunit 30270 "Shpfy Bulk Operation Mgt."
{
var
InvalidUserErr: Label 'You must sign in with a Business Central licensed user to enable the feature.';
- CategoryTok: Label 'Shopify Integration', Locked = true;
- BulkOperationsDontMatchLbl: Label 'Searched bulk operation (%1, %2, %3) does not match with current one (%4)', Comment = '%1 = Bulk Operation Id, %2 = Shop Code, %3 = Type, %4 = Bulk Operation Id', Locked = true;
BulkOperationCreatedLbl: Label 'A bulk request was sent to Shopify. You can check the status of the synchronization in the Shopify Bulk Operations page.';
internal procedure EnableBulkOperations(var Shop: Record "Shpfy Shop")
@@ -112,15 +110,14 @@ codeunit 30270 "Shpfy Bulk Operation Mgt."
var
BulkOperation: Record "Shpfy Bulk Operation";
BulkOperationAPI: Codeunit "Shpfy Bulk Operation API";
- BulkOperationId: BigInteger;
ErrorCode: Text;
CompletedAt: DateTime;
Url: Text;
PartialDataUrl: Text;
begin
BulkOperationAPI.SetShop(Shop);
- BulkOperationAPI.GetCurrentBulkRequest(BulkOperationId, BulkOperationStatus, ErrorCode, CompletedAt, Url, PartialDataUrl);
- if BulkOperation.Get(BulkOperationId, Shop.Code, Type) then begin
+ BulkOperationAPI.GetBulkRequest(SearchBulkOperationId, BulkOperationStatus, ErrorCode, CompletedAt, Url, PartialDataUrl);
+ if BulkOperation.Get(SearchBulkOperationId, Shop.Code, Type) then begin
BulkOperation.Status := BulkOperationStatus;
if ErrorCode <> '' then
BulkOperation."Error Code" := CopyStr(ErrorCode, 1, MaxStrLen(BulkOperation."Error Code"));
@@ -131,22 +128,6 @@ codeunit 30270 "Shpfy Bulk Operation Mgt."
if PartialDataUrl <> '' then
BulkOperation."Partial Data Url" := CopyStr(PartialDataUrl, 1, MaxStrLen(BulkOperation."Partial Data Url"));
BulkOperation.Modify(true);
-
- if BulkOperationId <> SearchBulkOperationId then begin
- Session.LogMessage('0000KZC', StrSubstNo(BulkOperationsDontMatchLbl, SearchBulkOperationId, Shop.Code, Type, BulkOperationId), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
- BulkOperationAPI.GetBulkRequest(SearchBulkOperationId, BulkOperationStatus, ErrorCode, CompletedAt, Url, PartialDataUrl);
- BulkOperation.Get(SearchBulkOperationId, Shop.Code, Type);
- BulkOperation.Status := BulkOperationStatus;
- if ErrorCode <> '' then
- BulkOperation."Error Code" := CopyStr(ErrorCode, 1, MaxStrLen(BulkOperation."Error Code"));
- if CompletedAt <> 0DT then
- BulkOperation."Completed At" := CompletedAt;
- if Url <> '' then
- BulkOperation.Url := CopyStr(Url, 1, MaxStrLen(BulkOperation.Url));
- if PartialDataUrl <> '' then
- BulkOperation."Partial Data Url" := CopyStr(PartialDataUrl, 1, MaxStrLen(BulkOperation."Partial Data Url"));
- BulkOperation.Modify(true);
- end;
end;
end;
diff --git a/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLBulkOperations.Codeunit.al b/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLBulkOperations.Codeunit.al
deleted file mode 100644
index af0a4d2520..0000000000
--- a/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLBulkOperations.Codeunit.al
+++ /dev/null
@@ -1,29 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
-namespace Microsoft.Integration.Shopify;
-
-codeunit 30277 "Shpfy GQL BulkOperations" implements "Shpfy IGraphQL"
-{
- Access = Internal;
-
- ///
- /// GetGraphQL.
- ///
- /// Return value of type Text.
- internal procedure GetGraphQL(): Text
- begin
- exit('{"query": "query { currentBulkOperation(type: MUTATION) { id status errorCode createdAt completedAt fileSize url partialDataUrl }}"}');
- end;
-
- ///
- /// GetExpectedCost.
- ///
- /// Return value of type Integer.
- internal procedure GetExpectedCost(): Integer
- begin
- exit(1);
- end;
-}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLModifyInventory.Codeunit.al b/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLModifyInventory.Codeunit.al
index 8c1db4e02f..0e3d7ab1f8 100644
--- a/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLModifyInventory.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLModifyInventory.Codeunit.al
@@ -9,7 +9,7 @@ codeunit 30102 "Shpfy GQL Modify Inventory" implements "Shpfy IGraphQL"
{
procedure GetGraphQL(): Text
begin
- exit('{"query":"mutation inventorySetOnHandQuantities($input:InventorySetOnHandQuantitiesInput!) { inventorySetOnHandQuantities(input: $input) { userErrors { field message }}}","variables":{"input":{"reason":"correction","setQuantities":[]}}}');
+ exit('{"query":"mutation inventorySetQuantities($input: InventorySetQuantitiesInput!) { inventorySetQuantities(input: $input) @idempotent(key: \"{{IdempotencyKey}}\") { inventoryAdjustmentGroup { id } userErrors { field message code }}}","variables":{"input":{"name":"on_hand","reason":"correction","quantities":[]}}}');
end;
procedure GetExpectedCost(): Integer
diff --git a/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLNextPayouts.Codeunit.al b/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLNextPayouts.Codeunit.al
index c07a419984..feef1f9531 100644
--- a/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLNextPayouts.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLNextPayouts.Codeunit.al
@@ -18,7 +18,7 @@ codeunit 30392 "Shpfy GQL NextPayouts" implements "Shpfy IGraphQL"
/// Return value of type Text.
procedure GetGraphQL(): Text
begin
- exit('{"query":"{ shopifyPaymentsAccount { payouts(first: 100, query: \"id:>={{SinceId}}\", after: \"{{After}}\") { edges { cursor node { id status summary { adjustmentsFee { amount } adjustmentsGross { amount } chargesFee { amount } chargesGross { amount } refundsFee { amount } refundsFeeGross { amount } reservedFundsFee { amount } reservedFundsGross { amount } retriedPayoutsFee { amount } retriedPayoutsGross { amount currencyCode } } issuedAt net { amount currencyCode } } } pageInfo { hasNextPage } } } }"}');
+ exit('{"query":"{ shopifyPaymentsAccount { payouts(first: 100, query: \"id:>={{SinceId}}\", after: \"{{After}}\") { edges { cursor node { id status externalTraceId summary { adjustmentsFee { amount } adjustmentsGross { amount } chargesFee { amount } chargesGross { amount } refundsFee { amount } refundsFeeGross { amount } reservedFundsFee { amount } reservedFundsGross { amount } retriedPayoutsFee { amount } retriedPayoutsGross { amount currencyCode } } issuedAt net { amount currencyCode } } } pageInfo { hasNextPage } } } }"}');
end;
///
diff --git a/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLNextReturnLines.Codeunit.al b/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLNextReturnLines.Codeunit.al
index 756372ae45..1873c080a8 100644
--- a/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLNextReturnLines.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLNextReturnLines.Codeunit.al
@@ -10,7 +10,7 @@ codeunit 30227 "Shpfy GQL NextReturnLines" implements "Shpfy IGraphQL"
internal procedure GetGraphQL(): Text
begin
- exit('{"query":"{ return(id: \"gid://shopify/Return/{{ReturnId}}\") { returnLineItems(first: 10, after:\"{{After}}\") { pageInfo { endCursor hasNextPage } nodes { id quantity returnReason returnReasonNote refundableQuantity refundedQuantity customerNote ... on UnverifiedReturnLineItem { __typename unitPrice { amount currencyCode } } ... on ReturnLineItem { __typename totalWeight { unit value } withCodeDiscountedTotalPriceSet { presentmentMoney { amount } shopMoney { amount } } fulfillmentLineItem { id lineItem { id } quantity originalTotalSet { presentmentMoney { amount } shopMoney { amount } } discountedTotalSet { presentmentMoney { amount } shopMoney { amount }}}}}}}}"}');
+ exit('{"query":"{ return(id: \"gid://shopify/Return/{{ReturnId}}\") { returnLineItems(first: 10, after:\"{{After}}\") { pageInfo { endCursor hasNextPage } nodes { id quantity returnReasonDefinition { name handle } returnReasonNote refundableQuantity refundedQuantity customerNote ... on UnverifiedReturnLineItem { __typename unitPrice { amount currencyCode } } ... on ReturnLineItem { __typename totalWeight { unit value } withCodeDiscountedTotalPriceSet { presentmentMoney { amount } shopMoney { amount } } fulfillmentLineItem { id lineItem { id } quantity originalTotalSet { presentmentMoney { amount } shopMoney { amount } } discountedTotalSet { presentmentMoney { amount } shopMoney { amount }}}}}}}}"}');
end;
internal procedure GetExpectedCost(): Integer
diff --git a/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLPayouts.Codeunit.al b/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLPayouts.Codeunit.al
index d28ed3aa6b..e5f259cc02 100644
--- a/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLPayouts.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLPayouts.Codeunit.al
@@ -18,7 +18,7 @@ codeunit 30391 "Shpfy GQL Payouts" implements "Shpfy IGraphQL"
/// Return value of type Text.
procedure GetGraphQL(): Text
begin
- exit('{"query":"{ shopifyPaymentsAccount { payouts(first: 100, query: \"id:>={{SinceId}}\") { edges { cursor node { id status summary { adjustmentsFee { amount } adjustmentsGross { amount } chargesFee { amount } chargesGross { amount } refundsFee { amount } refundsFeeGross { amount } reservedFundsFee { amount } reservedFundsGross { amount } retriedPayoutsFee { amount } retriedPayoutsGross { amount currencyCode } } issuedAt net { amount currencyCode } } } pageInfo { hasNextPage } } } }"}');
+ exit('{"query":"{ shopifyPaymentsAccount { payouts(first: 100, query: \"id:>={{SinceId}}\") { edges { cursor node { id status externalTraceId summary { adjustmentsFee { amount } adjustmentsGross { amount } chargesFee { amount } chargesGross { amount } refundsFee { amount } refundsFeeGross { amount } reservedFundsFee { amount } reservedFundsGross { amount } retriedPayoutsFee { amount } retriedPayoutsGross { amount currencyCode } } issuedAt net { amount currencyCode } } } pageInfo { hasNextPage } } } }"}');
end;
///
diff --git a/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLReturnLines.Codeunit.al b/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLReturnLines.Codeunit.al
index f1b108c697..92e157c75a 100644
--- a/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLReturnLines.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLReturnLines.Codeunit.al
@@ -10,7 +10,7 @@ codeunit 30226 "Shpfy GQL ReturnLines" implements "Shpfy IGraphQL"
internal procedure GetGraphQL(): Text
begin
- exit('{"query":"{ return(id: \"gid://shopify/Return/{{ReturnId}}\") { returnLineItems(first: 10) { pageInfo { endCursor hasNextPage } nodes { id quantity returnReason returnReasonNote refundableQuantity refundedQuantity customerNote ... on UnverifiedReturnLineItem { __typename unitPrice { amount currencyCode } } ... on ReturnLineItem { __typename totalWeight { unit value } withCodeDiscountedTotalPriceSet { presentmentMoney { amount } shopMoney { amount } } fulfillmentLineItem { id lineItem { id } quantity originalTotalSet { presentmentMoney { amount } shopMoney { amount } } discountedTotalSet { presentmentMoney { amount } shopMoney { amount }}}}}}}}"}');
+ exit('{"query":"{ return(id: \"gid://shopify/Return/{{ReturnId}}\") { returnLineItems(first: 10) { pageInfo { endCursor hasNextPage } nodes { id quantity returnReasonDefinition { name handle } returnReasonNote refundableQuantity refundedQuantity customerNote ... on UnverifiedReturnLineItem { __typename unitPrice { amount currencyCode } } ... on ReturnLineItem { __typename totalWeight { unit value } withCodeDiscountedTotalPriceSet { presentmentMoney { amount } shopMoney { amount } } fulfillmentLineItem { id lineItem { id } quantity originalTotalSet { presentmentMoney { amount } shopMoney { amount } } discountedTotalSet { presentmentMoney { amount } shopMoney { amount }}}}}}}}"}');
end;
internal procedure GetExpectedCost(): Integer
diff --git a/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLVariantById.Codeunit.al b/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLVariantById.Codeunit.al
index 85f306fa4e..5e5e86ae1f 100644
--- a/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLVariantById.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/GraphQL/Codeunits/ShpfyGQLVariantById.Codeunit.al
@@ -18,7 +18,7 @@ codeunit 30150 "Shpfy GQL VariantById" implements "Shpfy IGraphQL"
/// Return value of type Text.
internal procedure GetGraphQL(): Text
begin
- exit('{"query":"{productVariant(id: \"gid://shopify/ProductVariant/{{VariantId}}\") {createdAt updatedAt availableForSale barcode compareAtPrice displayName inventoryPolicy position price sku taxCode taxable title product{id}selectedOptions{name value} inventoryItem{countryCodeOfOrigin createdAt id inventoryHistoryUrl legacyResourceId measurement { weight { value }} provinceCodeOfOrigin requiresShipping sku tracked updatedAt unitCost { amount currencyCode }} metafields(first: 50) {edges {node {id namespace type legacyResourceId key value}}}}}"}');
+ exit('{"query":"{productVariant(id: \"gid://shopify/ProductVariant/{{VariantId}}\") {createdAt updatedAt availableForSale barcode compareAtPrice displayName inventoryPolicy position price sku taxable title product{id}selectedOptions{name value} inventoryItem{countryCodeOfOrigin createdAt id inventoryHistoryUrl legacyResourceId measurement { weight { value }} provinceCodeOfOrigin requiresShipping sku tracked updatedAt unitCost { amount currencyCode }} metafields(first: 50) {edges {node {id namespace type legacyResourceId key value}}}}}"}');
end;
///
diff --git a/src/Apps/W1/Shopify/App/src/GraphQL/Enums/ShpfyGraphQLType.Enum.al b/src/Apps/W1/Shopify/App/src/GraphQL/Enums/ShpfyGraphQLType.Enum.al
index f682cdf682..2d11e93d8d 100644
--- a/src/Apps/W1/Shopify/App/src/GraphQL/Enums/ShpfyGraphQLType.Enum.al
+++ b/src/Apps/W1/Shopify/App/src/GraphQL/Enums/ShpfyGraphQLType.Enum.al
@@ -295,11 +295,6 @@ enum 30111 "Shpfy GraphQL Type" implements "Shpfy IGraphQL"
Caption = 'Get Next Refund Lines';
Implementation = "Shpfy IGraphQL" = "Shpfy GQL NextRefundLines";
}
- value(57; GetCurrentBulkOperation)
- {
- Caption = 'Get Current Bulk Operation';
- Implementation = "Shpfy IGraphQL" = "Shpfy GQL BulkOperations";
- }
value(58; RunBulkOperationMutation)
{
Caption = 'Run Bulk Operation Mutation';
diff --git a/src/Apps/W1/Shopify/App/src/Inventory/Codeunits/ShpfyInventoryAPI.Codeunit.al b/src/Apps/W1/Shopify/App/src/Inventory/Codeunits/ShpfyInventoryAPI.Codeunit.al
index 69248f3162..4b199ac927 100644
--- a/src/Apps/W1/Shopify/App/src/Inventory/Codeunits/ShpfyInventoryAPI.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/Inventory/Codeunits/ShpfyInventoryAPI.Codeunit.al
@@ -141,44 +141,95 @@ codeunit 30195 "Shpfy Inventory API"
var
IGraphQL: Interface "Shpfy IGraphQL";
JGraphQL: JsonObject;
- JSetQuantities: JsonArray;
- JSetQuantity: JsonObject;
+ JQuantities: JsonArray;
+ JQuantity: JsonObject;
InputSize: Integer;
begin
if ShopInventory.FindSet() then begin
IGraphQL := Enum::"Shpfy GraphQL Type"::ModifyInventory;
JGraphQL.ReadFrom(IGraphQL.GetGraphQL());
- JSetQuantities := JsonHelper.GetJsonArray(JGraphQL, 'variables.input.setQuantities');
+ JQuantities := JsonHelper.GetJsonArray(JGraphQL, 'variables.input.quantities');
repeat
- JSetQuantity := CalcStock(ShopInventory);
- if JSetQuantity.Keys.Count = 3 then begin
- JSetQuantities.Add(JSetQuantity);
+ JQuantity := CalcStock(ShopInventory);
+ if JQuantity.Keys.Count = 4 then begin
+ JQuantities.Add(JQuantity);
InputSize += 1;
if InputSize = 250 then begin
- ShopifyCommunicationMgt.ExecuteGraphQL(Format(JGraphQL), IGraphQL.GetExpectedCost());
+ ExecuteInventoryGraphQL(JGraphQL, IGraphQL.GetExpectedCost());
Clear(JGraphQL);
JGraphQL.ReadFrom(IGraphQL.GetGraphQL());
- JSetQuantities := JsonHelper.GetJsonArray(JGraphQL, 'variables.input.setQuantities');
+ JQuantities := JsonHelper.GetJsonArray(JGraphQL, 'variables.input.quantities');
InputSize := 0;
end;
end;
until ShopInventory.Next() = 0;
- ShopifyCommunicationMgt.ExecuteGraphQL(Format(JGraphQL), IGraphQL.GetExpectedCost());
+ ExecuteInventoryGraphQL(JGraphQL, IGraphQL.GetExpectedCost());
end;
end;
- local procedure CalcStock(var ShopInventory: Record "Shpfy Shop Inventory") JSetQuantity: JsonObject
+ local procedure ExecuteInventoryGraphQL(JGraphQL: JsonObject; ExpectedCost: Integer)
+ var
+ JResponse: JsonToken;
+ JUserErrors: JsonArray;
+ GraphQLText: Text;
+ IdempotencyKey: Guid;
+ RetryAttempt: Integer;
+ MaxRetries: Integer;
+ HasConcurrencyError: Boolean;
+ ErrorCode: Text;
+ JError: JsonToken;
+ begin
+ MaxRetries := 1;
+ RetryAttempt := 0;
+
+ repeat
+ HasConcurrencyError := false;
+ IdempotencyKey := CreateGuid();
+ GraphQLText := Format(JGraphQL);
+ GraphQLText := GraphQLText.Replace('{{IdempotencyKey}}', Format(IdempotencyKey, 0, 4).TrimStart('{').TrimEnd('}'));
+
+ JResponse := ShopifyCommunicationMgt.ExecuteGraphQL(GraphQLText, ExpectedCost);
+
+ if JsonHelper.GetJsonArray(JResponse, JUserErrors, 'data.inventorySetQuantities.userErrors') then
+ foreach JError in JUserErrors do begin
+ ErrorCode := JsonHelper.GetValueAsText(JError, 'code');
+ if ErrorCode in ['IDEMPOTENCY_CONCURRENT_REQUEST', 'CHANGE_FROM_QUANTITY_STALE'] then begin
+ HasConcurrencyError := true;
+ break;
+ end;
+ end;
+
+ RetryAttempt += 1;
+ until (not HasConcurrencyError) or (RetryAttempt > MaxRetries);
+
+ if HasConcurrencyError then
+ LogSkippedInventoryUpdate(JGraphQL, ErrorCode);
+ end;
+
+ local procedure LogSkippedInventoryUpdate(JGraphQL: JsonObject; ErrorCode: Text)
+ var
+ SkippedRecord: Codeunit "Shpfy Skipped Record";
+ EmptyRecordId: RecordId;
+ JQuantities: JsonArray;
+ SkippedMsg: Label 'Inventory update skipped after retry due to %1 error', Comment = '%1 = Error code';
+ begin
+ if JsonHelper.GetJsonArray(JGraphQL, JQuantities, 'variables.input.quantities') then
+ if JQuantities.Count > 0 then
+ SkippedRecord.LogSkippedRecord(EmptyRecordId, CopyStr(StrSubstNo(SkippedMsg, ErrorCode), 1, 250), ShopifyShop);
+ end;
+
+ local procedure CalcStock(var ShopInventory: Record "Shpfy Shop Inventory") JQuantity: JsonObject
var
Item: Record Item;
DelShopInventory: Record "Shpfy Shop Inventory";
ShopLocation: Record "Shpfy Shop Location";
ShopifyVariant: Record "Shpfy Variant";
IStockAvailable: Interface "Shpfy IStock Available";
+ JNull: JsonValue;
InventoryItemIdTxt: Label 'gid://shopify/InventoryItem/%1', Locked = true, Comment = '%1 = The inventory Item Id';
LocationIdTxt: Label 'gid://shopify/Location/%1', Locked = true, Comment = '%1 = The Location Id';
-
begin
ShopifyVariant.SetRange(Id, ShopInventory."Variant Id");
if ShopifyVariant.IsEmpty then begin
@@ -196,12 +247,14 @@ codeunit 30195 "Shpfy Inventory API"
if ShopLocation.Get(ShopInventory."Shop Code", ShopInventory."Location Id") then begin
IStockAvailable := ShopLocation."Stock Calculation";
if IStockAvailable.CanHaveStock() then begin
- JSetQuantity.Add('inventoryItemId', StrSubstNo(InventoryItemIdTxt, ShopInventory."Inventory Item Id"));
- JSetQuantity.Add('locationId', StrSubstNo(LocationIdTxt, ShopLocation.Id));
+ JQuantity.Add('inventoryItemId', StrSubstNo(InventoryItemIdTxt, ShopInventory."Inventory Item Id"));
+ JQuantity.Add('locationId', StrSubstNo(LocationIdTxt, ShopLocation.Id));
if ShopInventory.Stock < 0 then
- JSetQuantity.Add('quantity', 0)
+ JQuantity.Add('quantity', 0)
else
- JSetQuantity.Add('quantity', ShopInventory.Stock);
+ JQuantity.Add('quantity', ShopInventory.Stock);
+ JNull.SetValueToNull();
+ JQuantity.Add('changeFromQuantity', JNull);
end;
end;
end;
diff --git a/src/Apps/W1/Shopify/App/src/Metafields/Codeunits/IMetafieldType/ShpfyMtfldTypeArticleRef.Codeunit.al b/src/Apps/W1/Shopify/App/src/Metafields/Codeunits/IMetafieldType/ShpfyMtfldTypeArticleRef.Codeunit.al
new file mode 100644
index 0000000000..2c769bb89a
--- /dev/null
+++ b/src/Apps/W1/Shopify/App/src/Metafields/Codeunits/IMetafieldType/ShpfyMtfldTypeArticleRef.Codeunit.al
@@ -0,0 +1,34 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Integration.Shopify;
+
+using System.Utilities;
+
+codeunit 30457 "Shpfy Mtfld Type Article Ref" implements "Shpfy IMetafield Type"
+{
+ procedure HasAssistEdit(): Boolean
+ begin
+ exit(false);
+ end;
+
+ procedure IsValidValue(Value: Text): Boolean
+ var
+ Regex: Codeunit Regex;
+ begin
+ exit(Regex.IsMatch(Value, '^gid:\/\/shopify\/Article\/\d+$'));
+ end;
+
+ procedure AssistEdit(var Value: Text[2048]): Boolean
+ begin
+ Value := Value;
+ exit(false);
+ end;
+
+ procedure GetExampleValue(): Text
+ begin
+ exit('gid://shopify/Article/1234567890');
+ end;
+}
diff --git a/src/Apps/W1/Shopify/App/src/Metafields/Enums/ShpfyMetafieldType.Enum.al b/src/Apps/W1/Shopify/App/src/Metafields/Enums/ShpfyMetafieldType.Enum.al
index 46856e0619..77dd4b1fde 100644
--- a/src/Apps/W1/Shopify/App/src/Metafields/Enums/ShpfyMetafieldType.Enum.al
+++ b/src/Apps/W1/Shopify/App/src/Metafields/Enums/ShpfyMetafieldType.Enum.al
@@ -172,4 +172,10 @@ enum 30159 "Shpfy Metafield Type" implements "Shpfy IMetafield Type"
Caption = 'Company';
Implementation = "Shpfy IMetafield Type" = "Shpfy Mtfld Type Company Ref";
}
+
+ value(27; article_reference)
+ {
+ Caption = 'Article';
+ Implementation = "Shpfy IMetafield Type" = "Shpfy Mtfld Type Article Ref";
+ }
}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/App/src/Order Returns/Codeunits/ShpfyReturnsAPI.Codeunit.al b/src/Apps/W1/Shopify/App/src/Order Returns/Codeunits/ShpfyReturnsAPI.Codeunit.al
index 295927b8a3..64c59e097f 100644
--- a/src/Apps/W1/Shopify/App/src/Order Returns/Codeunits/ShpfyReturnsAPI.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/Order Returns/Codeunits/ShpfyReturnsAPI.Codeunit.al
@@ -204,7 +204,8 @@ codeunit 30250 "Shpfy Returns API"
ReturnLine."Order Line Id" := CommunicationMgt.GetIdOfGId(JsonHelper.GetValueAsText(JLine, 'fulfillmentLineItem.lineItem.id'));
ReturnLine.Insert();
end;
- ReturnLine."Return Reason" := ReturnEnumConvertor.ConvertToReturnReason(JsonHelper.GetValueAsText(JLine, 'returnReason'));
+ ReturnLine."Return Reason Name" := CopyStr(JsonHelper.GetValueAsText(JLine, 'returnReasonDefinition.name'), 1, MaxStrLen(ReturnLine."Return Reason Name"));
+ ReturnLine."Return Reason Handle" := CopyStr(JsonHelper.GetValueAsText(JLine, 'returnReasonDefinition.handle'), 1, MaxStrLen(ReturnLine."Return Reason Handle"));
// If item was restocked to multiple locations, we cannot determine the return location for the line
if ReturnLocations.Get(ReturnLine."Order Line Id", ReturnLocation) then
ReturnLine."Location Id" := ReturnLocation;
@@ -239,7 +240,8 @@ codeunit 30250 "Shpfy Returns API"
ReturnLine.Type := ReturnLine.Type::Unverified;
ReturnLine.Insert();
end;
- ReturnLine."Return Reason" := ReturnEnumConvertor.ConvertToReturnReason(JsonHelper.GetValueAsText(JLine, 'returnReason'));
+ ReturnLine."Return Reason Name" := CopyStr(JsonHelper.GetValueAsText(JLine, 'returnReasonDefinition.name'), 1, MaxStrLen(ReturnLine."Return Reason Name"));
+ ReturnLine."Return Reason Handle" := CopyStr(JsonHelper.GetValueAsText(JLine, 'returnReasonDefinition.handle'), 1, MaxStrLen(ReturnLine."Return Reason Handle"));
ReturnLine.SetReturnReasonNote(JsonHelper.GetValueAsText(JLine, 'returnReasonNote'));
ReturnLine.SetCustomerNote(JsonHelper.GetValueAsText(JLine, 'customerNote'));
diff --git a/src/Apps/W1/Shopify/App/src/Order Returns/Pages/ShpfyReturnLines.Page.al b/src/Apps/W1/Shopify/App/src/Order Returns/Pages/ShpfyReturnLines.Page.al
index c933401861..d6ee69df6b 100644
--- a/src/Apps/W1/Shopify/App/src/Order Returns/Pages/ShpfyReturnLines.Page.al
+++ b/src/Apps/W1/Shopify/App/src/Order Returns/Pages/ShpfyReturnLines.Page.al
@@ -42,7 +42,18 @@ page 30149 "Shpfy Return Lines"
ApplicationArea = All;
ToolTip = 'Specifies the quantity being returned.';
}
+#if not CLEAN28
field("Return Reason"; Rec."Return Reason")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Specifies the reason for returning the item.';
+ Visible = false;
+ ObsoleteState = Pending;
+ ObsoleteReason = 'Shopify API 2025-10 deprecated returnReason. Use Return Reason Name field instead.';
+ ObsoleteTag = '28.0';
+ }
+#endif
+ field("Return Reason Name"; Rec."Return Reason Name")
{
ApplicationArea = All;
ToolTip = 'Specifies the reason for returning the item.';
diff --git a/src/Apps/W1/Shopify/App/src/Order Returns/Tables/ShpfyReturnLine.Table.al b/src/Apps/W1/Shopify/App/src/Order Returns/Tables/ShpfyReturnLine.Table.al
index fce184e211..cbad38ff99 100644
--- a/src/Apps/W1/Shopify/App/src/Order Returns/Tables/ShpfyReturnLine.Table.al
+++ b/src/Apps/W1/Shopify/App/src/Order Returns/Tables/ShpfyReturnLine.Table.al
@@ -47,12 +47,22 @@ table 30141 "Shpfy Return Line"
DataClassification = SystemMetadata;
Editable = false;
}
+#if not CLEANSCHEMA31
field(6; "Return Reason"; Enum "Shpfy Return Reason")
{
Caption = 'Return Reason';
DataClassification = SystemMetadata;
Editable = false;
+ ObsoleteReason = 'Replaced by Return Reason Name and Return Reason Handle fields. Shopify API 2026-01 deprecated returnReason in favor of returnReasonDefinition.';
+#if not CLEAN28
+ ObsoleteState = Pending;
+ ObsoleteTag = '28.0';
+#else
+ ObsoleteState = Removed;
+ ObsoleteTag = '31.0';
+#endif
}
+#endif
field(7; "Return Reason Note"; Blob)
{
Caption = 'Return Reason Note';
@@ -125,6 +135,18 @@ table 30141 "Shpfy Return Line"
DataClassification = SystemMetadata;
Editable = false;
}
+ field(18; "Return Reason Name"; Text[100])
+ {
+ Caption = 'Return Reason';
+ DataClassification = SystemMetadata;
+ Editable = false;
+ }
+ field(19; "Return Reason Handle"; Text[100])
+ {
+ Caption = 'Return Reason Handle';
+ DataClassification = SystemMetadata;
+ Editable = false;
+ }
field(101; "Item No."; Code[20])
{
Caption = 'Item No.';
diff --git a/src/Apps/W1/Shopify/App/src/Payments/Codeunits/ShpfyPaymentsAPI.Codeunit.al b/src/Apps/W1/Shopify/App/src/Payments/Codeunits/ShpfyPaymentsAPI.Codeunit.al
index 1fdf987b43..45a2af07e7 100644
--- a/src/Apps/W1/Shopify/App/src/Payments/Codeunits/ShpfyPaymentsAPI.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/Payments/Codeunits/ShpfyPaymentsAPI.Codeunit.al
@@ -115,7 +115,7 @@ codeunit 30385 "Shpfy Payments API"
until not JsonHelper.GetValueAsBoolean(JResponse, 'data.shopifyPaymentsAccount.payouts.pageInfo.hasNextPage');
end;
- local procedure ImportPayout(JPayout: JsonObject)
+ internal procedure ImportPayout(JPayout: JsonObject)
var
DataCapture: Record "Shpfy Data Capture";
Payout: Record "Shpfy Payout";
@@ -142,6 +142,7 @@ codeunit 30385 "Shpfy Payments API"
JsonHelper.GetValueIntoField(JPayout, 'summary.reservedFundsGross.amount', RecordRef, Payout.FieldNo("Reserved Funds Gross Amount"));
JsonHelper.GetValueIntoField(JPayout, 'summary.retriedPayoutsFee.amount', RecordRef, Payout.FieldNo("Retried Payouts Fee Amount"));
JsonHelper.GetValueIntoField(JPayout, 'summary.retriedPayoutsGross.amount', RecordRef, Payout.FieldNo("Retried Payouts Gross Amount"));
+ JsonHelper.GetValueIntoField(JPayout, 'externalTraceId', RecordRef, Payout.FieldNo("External Trace Id"));
RecordRef.SetTable(Payout);
RecordRef.Close();
Payout.Id := Id;
diff --git a/src/Apps/W1/Shopify/App/src/Payments/Tables/ShpfyPayout.Table.al b/src/Apps/W1/Shopify/App/src/Payments/Tables/ShpfyPayout.Table.al
index 876cf78296..fae556b580 100644
--- a/src/Apps/W1/Shopify/App/src/Payments/Tables/ShpfyPayout.Table.al
+++ b/src/Apps/W1/Shopify/App/src/Payments/Tables/ShpfyPayout.Table.al
@@ -112,6 +112,11 @@ table 30125 "Shpfy Payout"
AutoFormatType = 1;
AutoFormatExpression = Currency;
}
+ field(16; "External Trace Id"; Text[250])
+ {
+ Caption = 'External Trace Id';
+ DataClassification = CustomerContent;
+ }
}
keys
{
diff --git a/src/Apps/W1/Shopify/App/src/PermissionSets/ShpfyObjects.PermissionSet.al b/src/Apps/W1/Shopify/App/src/PermissionSets/ShpfyObjects.PermissionSet.al
index 92928afb81..6f1c2fa0c7 100644
--- a/src/Apps/W1/Shopify/App/src/PermissionSets/ShpfyObjects.PermissionSet.al
+++ b/src/Apps/W1/Shopify/App/src/PermissionSets/ShpfyObjects.PermissionSet.al
@@ -161,7 +161,6 @@ permissionset 30104 "Shpfy - Objects"
codeunit "Shpfy GQL ApiKey" = X,
codeunit "Shpfy GQL AssignedFFOrders" = X,
codeunit "Shpfy GQL BulkOperation" = X,
- codeunit "Shpfy GQL BulkOperations" = X,
codeunit "Shpfy GQL BulkOpMutation" = X,
codeunit "Shpfy GQL Catalog Markets" = X,
codeunit "Shpfy GQL CatalogPrices" = X,
diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al
index f690c24b19..5b1c372562 100644
--- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyCreateProduct.Codeunit.al
@@ -101,7 +101,6 @@ codeunit 30174 "Shpfy Create Product"
TempShopifyVariant.Title := ItemVariant.Description;
TempShopifyVariant."Inventory Policy" := Shop."Default Inventory Policy";
TempShopifyVariant.SKU := GetVariantSKU(TempShopifyVariant.Barcode, Item."No.", ItemVariant.Code, Item."Vendor Item No.");
- TempShopifyVariant."Tax Code" := Item."Tax Group Code";
TempShopifyVariant.Taxable := true;
TempShopifyVariant.Weight := ItemUnitofMeasure."Qty. per Unit of Measure" > 0 ? Item."Gross Weight" * ItemUnitofMeasure."Qty. per Unit of Measure" : Item."Gross Weight";
TempShopifyVariant."Option 1 Name" := 'Variant';
@@ -124,7 +123,6 @@ codeunit 30174 "Shpfy Create Product"
TempShopifyVariant.Title := ItemVariant.Description;
TempShopifyVariant."Inventory Policy" := Shop."Default Inventory Policy";
TempShopifyVariant.SKU := GetVariantSKU(TempShopifyVariant.Barcode, Item."No.", ItemVariant.Code, GetVendorItemNo(Item."No.", ItemVariant.Code, Item."Sales Unit of Measure"));
- TempShopifyVariant."Tax Code" := Item."Tax Group Code";
TempShopifyVariant.Taxable := true;
TempShopifyVariant.Weight := Item."Gross Weight";
TempShopifyVariant."Option 1 Name" := 'Variant';
@@ -151,7 +149,6 @@ codeunit 30174 "Shpfy Create Product"
TempShopifyVariant.Title := Item.Description;
TempShopifyVariant."Inventory Policy" := Shop."Default Inventory Policy";
TempShopifyVariant.SKU := GetVariantSKU(TempShopifyVariant.Barcode, Item."No.", '', Item."Vendor Item No.");
- TempShopifyVariant."Tax Code" := Item."Tax Group Code";
TempShopifyVariant.Taxable := true;
TempShopifyVariant.Weight := ItemUnitofMeasure."Qty. per Unit of Measure" > 0 ? Item."Gross Weight" * ItemUnitofMeasure."Qty. per Unit of Measure" : Item."Gross Weight";
TempShopifyVariant."Option 1 Name" := Shop."Option Name for UoM";
@@ -232,7 +229,6 @@ codeunit 30174 "Shpfy Create Product"
TempShopifyVariant.Title := ''; // Title will be assigned to "Default Title" in Shopify as no Options are set.
TempShopifyVariant."Inventory Policy" := Shop."Default Inventory Policy";
TempShopifyVariant.SKU := GetVariantSKU(TempShopifyVariant.Barcode, Item."No.", '', Item."Vendor Item No.");
- TempShopifyVariant."Tax Code" := Item."Tax Group Code";
TempShopifyVariant.Taxable := true;
TempShopifyVariant.Weight := Item."Gross Weight";
TempShopifyVariant."Shop Code" := Shop.Code;
diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al
index 208621e2ac..4bc95e198e 100644
--- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductExport.Codeunit.al
@@ -358,7 +358,6 @@ codeunit 30178 "Shpfy Product Export"
Shop."SKU Mapping"::"Vendor Item No.":
ShopifyVariant.SKU := Item."Vendor Item No.";
end;
- ShopifyVariant."Tax Code" := Item."Tax Group Code";
ShopifyVariant.Taxable := true;
ShopifyVariant.Weight := ItemUnitofMeasure."Qty. per Unit of Measure" > 0 ? Item."Gross Weight" * ItemUnitofMeasure."Qty. per Unit of Measure" : Item."Gross Weight";
ShopifyVariant."Option 1 Name" := Shop."Option Name for UoM";
@@ -415,7 +414,6 @@ codeunit 30178 "Shpfy Product Export"
Shop."SKU Mapping"::"Vendor Item No.":
ShopifyVariant.SKU := Item."Vendor Item No.";
end;
- ShopifyVariant."Tax Code" := Item."Tax Group Code";
ShopifyVariant.Taxable := true;
ShopifyVariant.Weight := Item."Gross Weight";
if ShopifyVariant."Option 1 Name" = '' then
@@ -470,7 +468,6 @@ codeunit 30178 "Shpfy Product Export"
Shop."SKU Mapping"::"Vendor Item No.":
ShopifyVariant.SKU := Item."Vendor Item No.";
end;
- ShopifyVariant."Tax Code" := Item."Tax Group Code";
ShopifyVariant.Taxable := true;
ShopifyVariant.Weight := ItemUnitofMeasure."Qty. per Unit of Measure" > 0 ? Item."Gross Weight" * ItemUnitofMeasure."Qty. per Unit of Measure" : Item."Gross Weight";
ShopifyVariant."Option 1 Name" := 'Variant';
diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyVariantAPI.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyVariantAPI.Codeunit.al
index 3c14ad5050..d8ced8f1e7 100644
--- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyVariantAPI.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyVariantAPI.Codeunit.al
@@ -99,8 +99,11 @@ codeunit 30189 "Shpfy Variant API"
JResponse: JsonToken;
JVariants: JsonArray;
ReturnQuery: Text;
+ VariantsInBatch: Integer;
+ MaxVariantsPerBatch: Integer;
begin
ReturnQuery := ']) {productVariants {legacyResourceId, createdAt, updatedAt}, userErrors {field, message}}}"}';
+ MaxVariantsPerBatch := GetMaxVariantsPerBatch();
if ShopifyVariant.FindSet() then begin
InventoryQuantities := GetInventoryQuantities();
@@ -109,13 +112,16 @@ codeunit 30189 "Shpfy Variant API"
GraphQuery.Append('\", strategy: ');
GraphQuery.Append(Format(Strategy));
GraphQuery.Append(', variants: [');
+ VariantsInBatch := 0;
repeat
ShopifyVariant."Product Id" := ProductId;
VariantGraphQuery := GetVariantGraphQuery(ShopifyVariant, InventoryQuantities);
- if GraphQuery.Length() + VariantGraphQuery.Length() + StrLen(ReturnQuery) < CommunicationMgt.GetGraphQueryLengthThreshold() then begin
+ if (GraphQuery.Length() + VariantGraphQuery.Length() + StrLen(ReturnQuery) < CommunicationMgt.GetGraphQueryLengthThreshold()) and
+ (VariantsInBatch < MaxVariantsPerBatch) then begin
GraphQuery.Append(VariantGraphQuery.ToText() + ', ');
TempNewShopifyVariant := ShopifyVariant;
TempNewShopifyVariant.Insert();
+ VariantsInBatch += 1;
end else begin
GraphQuery.Remove(GraphQuery.Length - 1, 2);
GraphQuery.Append(ReturnQuery);
@@ -133,6 +139,7 @@ codeunit 30189 "Shpfy Variant API"
GraphQuery.Append(Format(Strategy));
GraphQuery.Append(', variants: [');
GraphQuery.Append(VariantGraphQuery.ToText() + ', ');
+ VariantsInBatch := 1;
end;
until ShopifyVariant.Next() = 0;
GraphQuery.Remove(GraphQuery.Length - 1, 2);
@@ -166,12 +173,6 @@ codeunit 30189 "Shpfy Variant API"
end;
if ShopifyVariant.Taxable then
GraphQuery.Append(', taxable: true');
- if ShopifyVariant."Tax Code" <> xShopifyVariant."Tax Code" then begin
- HasChange := true;
- GraphQuery.Append(', taxCode: \"');
- GraphQuery.Append(ShopifyVariant."Tax Code");
- GraphQuery.Append('\"');
- end;
if ShopifyVariant.Price <> xShopifyVariant.Price then begin
HasChange := true;
GraphQuery.Append(', price: \"');
@@ -236,11 +237,6 @@ codeunit 30189 "Shpfy Variant API"
end;
if ShopifyVariant.Taxable then
GraphQuery.Append(', taxable: true');
- if ShopifyVariant."Tax Code" <> '' then begin
- GraphQuery.Append(', taxCode: \"');
- GraphQuery.Append(ShopifyVariant."Tax Code");
- GraphQuery.Append('\"');
- end;
if ShopifyVariant.Price > 0 then begin
GraphQuery.Append(', price: \"');
GraphQuery.Append(Format(ShopifyVariant.Price, 0, 9));
@@ -323,6 +319,28 @@ codeunit 30189 "Shpfy Variant API"
exit(GraphQuery.ToText());
end;
+ internal procedure GetDefaultLocationCount(): Integer
+ var
+ ShopLocation: Record "Shpfy Shop Location";
+ begin
+ ShopLocation.SetRange("Shop Code", Shop.Code);
+ ShopLocation.SetRange(Active, true);
+ ShopLocation.SetRange("Default Product Location", true);
+ exit(ShopLocation.Count());
+ end;
+
+ internal procedure GetMaxVariantsPerBatch(): Integer
+ var
+ LocationCount: Integer;
+ MaxInventoryQuantities: Integer;
+ begin
+ MaxInventoryQuantities := 50000;
+ LocationCount := GetDefaultLocationCount();
+ if LocationCount = 0 then
+ exit(250); // Default batch size when no inventory quantities are added
+ exit(MaxInventoryQuantities div LocationCount);
+ end;
+
local procedure CreateNewVariant(JVariant: JsonToken; var ShopifyVariant: Record "Shpfy Variant"; ProductId: BigInteger): Boolean
var
NewShopifyVariant: Record "Shpfy Variant";
@@ -756,7 +774,6 @@ codeunit 30189 "Shpfy Variant API"
#pragma warning disable AA0139
ShopifyVariant.Barcode := JsonHelper.GetValueAsText(JVariant, 'barcode', MaxStrLen(ShopifyVariant.Barcode));
ShopifyVariant."Display Name" := JsonHelper.GetValueAsText(JVariant, 'displayName', MaxStrLen(ShopifyVariant."Display Name"));
- ShopifyVariant."Tax Code" := JsonHelper.GetValueAsText(JVariant, 'taxCode', MaxStrLen(ShopifyVariant."Tax Code"));
ShopifyVariant.SKU := JsonHelper.GetValueAsText(JVariant, 'sku', MaxStrLen(ShopifyVariant.SKU));
ShopifyVariant.Title := JsonHelper.GetValueAsText(JVariant, 'title', MaxStrLen(ShopifyVariant.Title));
#pragma warning restore AA0139
diff --git a/src/Apps/W1/Shopify/App/src/Products/Pages/ShpfyVariants.Page.al b/src/Apps/W1/Shopify/App/src/Products/Pages/ShpfyVariants.Page.al
index 32715fd204..be3b408cb0 100644
--- a/src/Apps/W1/Shopify/App/src/Products/Pages/ShpfyVariants.Page.al
+++ b/src/Apps/W1/Shopify/App/src/Products/Pages/ShpfyVariants.Page.al
@@ -144,11 +144,17 @@ page 30127 "Shpfy Variants"
ApplicationArea = All;
ToolTip = 'Specifies whether a tax is charged when the product variant is sold.';
}
+#if not CLEAN28
field(TaxCode; Rec."Tax Code")
{
ApplicationArea = All;
ToolTip = 'Specifies the Avalara tax code for the product variant. This parameter applies only to the stores that have the Avalara AvaTax app installed.';
+ Visible = false;
+ ObsoleteState = Pending;
+ ObsoleteReason = 'Shopify API 2025-10 deprecated taxCode on ProductVariant. This field is no longer available in the API.';
+ ObsoleteTag = '28.0';
}
+#endif
field(UnitCost; Rec."Unit Cost")
{
ApplicationArea = All;
diff --git a/src/Apps/W1/Shopify/App/src/Products/Tables/ShpfyVariant.Table.al b/src/Apps/W1/Shopify/App/src/Products/Tables/ShpfyVariant.Table.al
index 367d41ca53..9538e494cf 100644
--- a/src/Apps/W1/Shopify/App/src/Products/Tables/ShpfyVariant.Table.al
+++ b/src/Apps/W1/Shopify/App/src/Products/Tables/ShpfyVariant.Table.al
@@ -81,11 +81,21 @@ table 30129 "Shpfy Variant"
Caption = 'SKU';
DataClassification = CustomerContent;
}
+#if not CLEANSCHEMA31
field(13; "Tax Code"; Code[20])
{
Caption = 'Tax Code';
DataClassification = CustomerContent;
+ ObsoleteReason = 'Shopify API 2025-10 deprecated taxCode on ProductVariant. This field is no longer available in the API.';
+#if not CLEAN28
+ ObsoleteState = Pending;
+ ObsoleteTag = '28.0';
+#else
+ ObsoleteState = Removed;
+ ObsoleteTag = '31.0';
+#endif
}
+#endif
field(14; Taxable; Boolean)
{
Caption = 'Taxable';
diff --git a/src/Apps/W1/Shopify/Test/.resources/Bulk Operations/CurrentBulkOperationCompletedResult.txt b/src/Apps/W1/Shopify/Test/.resources/Bulk Operations/CurrentBulkOperationCompletedResult.txt
deleted file mode 100644
index a248d0ccc0..0000000000
--- a/src/Apps/W1/Shopify/Test/.resources/Bulk Operations/CurrentBulkOperationCompletedResult.txt
+++ /dev/null
@@ -1 +0,0 @@
-{ "data": { "currentBulkOperation": { "id": "gid://shopify/BulkOperation/%1", "status": "COMPLETED", "errorCode": null, "createdAt": "2021-01-28T19:10:59Z", "completedAt": "2021-01-28T19:11:09Z", "objectCount": "16", "fileSize": "0", "url": "", "partialDataUrl": null } }, "extensions": { "cost": { "requestedQueryCost": 1, "actualQueryCost": 1 } } }
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/Test/.resources/Bulk Operations/CurrentBulkOperationRunningResult.txt b/src/Apps/W1/Shopify/Test/.resources/Bulk Operations/CurrentBulkOperationRunningResult.txt
deleted file mode 100644
index 9f42cae022..0000000000
--- a/src/Apps/W1/Shopify/Test/.resources/Bulk Operations/CurrentBulkOperationRunningResult.txt
+++ /dev/null
@@ -1 +0,0 @@
-{ "data": { "currentBulkOperation": { "id": "gid://shopify/BulkOperation/%1", "status": "RUNNING", "errorCode": null, "createdAt": "2021-01-28T19:10:59Z", "completedAt": "2021-01-28T19:11:09Z", "objectCount": "16", "fileSize": "0", "url": "", "partialDataUrl": null } }, "extensions": { "cost": { "requestedQueryCost": 1, "actualQueryCost": 1 } } }
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/Test/Bulk Operations/Codeunits/ShpfyBulkOpSubscriber.Codeunit.al b/src/Apps/W1/Shopify/Test/Bulk Operations/Codeunits/ShpfyBulkOpSubscriber.Codeunit.al
index ede01420a6..bd4253af09 100644
--- a/src/Apps/W1/Shopify/Test/Bulk Operations/Codeunits/ShpfyBulkOpSubscriber.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Bulk Operations/Codeunits/ShpfyBulkOpSubscriber.Codeunit.al
@@ -59,7 +59,6 @@ codeunit 139615 "Shpfy Bulk Op. Subscriber"
GraphQLQuery: Text;
StagedUploadGQLTxt: Label '{"query": "mutation { stagedUploadsCreate(input', Locked = true;
BulkMutationGQLTxt: Label '{"query": "mutation { bulkOperationRunMutation(mutation', Locked = true;
- CurrentBulkOperationGQLTxt: Label '{"query": "query { currentBulkOperation(type', Locked = true;
BulkOperationGQLTxt: Label '{"query": "query { node(id: \"gid://shopify/BulkOperation/', Locked = true;
GraphQLCmdTxt: Label '/graphql.json', Locked = true;
begin
@@ -73,11 +72,6 @@ codeunit 139615 "Shpfy Bulk Op. Subscriber"
HttpResponseMessage := GetStagedUplodResult();
if GraphQLQuery.StartsWith(BulkMutationGQLTxt) then
HttpResponseMessage := GetBulkMutationResponse();
- if GraphQLQuery.StartsWith(CurrentBulkOperationGQLTxt) then
- if BulkOperationRunning then
- HttpResponseMessage := GetCurrentBulkOperationRunningResult()
- else
- HttpResponseMessage := GetCurrentBulkOperationCompletedResult();
if GraphQLQuery.StartsWith(BulkOperationGQLTxt) then
HttpResponseMessage := GetBulkOperationCompletedResult();
end;
@@ -115,30 +109,6 @@ codeunit 139615 "Shpfy Bulk Op. Subscriber"
exit(HttpResponseMessage);
end;
- local procedure GetCurrentBulkOperationCompletedResult(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Bulk Operations/CurrentBulkOperationCompletedResult.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- HttpResponseMessage.Content.WriteFrom(StrSubstNo(Body, Format(BulkOperationId)));
- exit(HttpResponseMessage);
- end;
-
- local procedure GetCurrentBulkOperationRunningResult(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Bulk Operations/CurrentBulkOperationRunningResult.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- HttpResponseMessage.Content.WriteFrom(StrSubstNo(Body, Format(BulkOperationId)));
- exit(HttpResponseMessage);
- end;
-
local procedure GetJsonlUploadResult(): HttpResponseMessage
var
HttpResponseMessage: HttpResponseMessage;
diff --git a/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventoryExportTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventoryExportTest.Codeunit.al
new file mode 100644
index 0000000000..c043d9f3cc
--- /dev/null
+++ b/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventoryExportTest.Codeunit.al
@@ -0,0 +1,347 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Integration.Shopify.Test;
+
+using Microsoft.Integration.Shopify;
+using Microsoft.Inventory.Item;
+using Microsoft.Inventory.Journal;
+using System.TestLibraries.Utilities;
+
+///
+/// Codeunit Shpfy Inventory Export Test (ID 139619).
+/// Tests for inventory export functionality including idempotency and retry logic.
+///
+codeunit 139619 "Shpfy Inventory Export Test"
+{
+ Subtype = Test;
+ TestType = IntegrationTest;
+ TestPermissions = Disabled;
+
+ var
+ Any: Codeunit Any;
+ LibraryAssert: Codeunit "Library Assert";
+ LibraryInventory: Codeunit "Library - Inventory";
+ IsInitialized: Boolean;
+
+ local procedure Initialize()
+ begin
+ if IsInitialized then
+ exit;
+ IsInitialized := true;
+ Codeunit.Run(Codeunit::"Shpfy Initialize Test");
+ end;
+
+ [Test]
+ procedure UnitTestExportStockSuccess()
+ var
+ Shop: Record "Shpfy Shop";
+ ShopLocation: Record "Shpfy Shop Location";
+ Item: Record Item;
+ ShopifyProduct: Record "Shpfy Product";
+ ShopInventory: Record "Shpfy Shop Inventory";
+ InventorySubscriber: Codeunit "Shpfy Inventory Subscriber";
+ CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
+ InventoryAPI: Codeunit "Shpfy Inventory API";
+ StockCalculate: Enum "Shpfy Stock Calculation";
+ begin
+ // [SCENARIO] Export stock successfully updates inventory in Shopify
+ // [GIVEN] A ShopInventory record with stock different from Shopify stock
+ Initialize();
+
+ Shop := CommunicationMgt.GetShopRecord();
+ CreateShopLocation(ShopLocation, Shop.Code, StockCalculate::"Projected Available Balance Today");
+ CreateItem(Item);
+ UpdateItemInventory(Item, 10);
+ CreateShpfyProduct(ShopifyProduct, ShopInventory, Item.SystemId, Shop.Code, ShopLocation.Id);
+ ShopInventory."Shopify Stock" := 5; // Different from calculated stock to trigger export
+ ShopInventory.Modify();
+
+ // [GIVEN] The inventory subscriber is configured to return success
+ BindSubscription(InventorySubscriber);
+ InventorySubscriber.SetRetryScenario(Enum::"Shpfy Inventory Retry Scenario"::Success);
+ InventoryAPI.SetShop(Shop.Code);
+
+ // [WHEN] ExportStock is called
+ ShopInventory.SetRange("Shop Code", Shop.Code);
+ ShopInventory.SetRange("Variant Id", ShopInventory."Variant Id");
+ InventoryAPI.ExportStock(ShopInventory);
+
+ // [THEN] The mutation was executed successfully (verified by subscriber not throwing error)
+ UnbindSubscription(InventorySubscriber);
+ end;
+
+ [Test]
+ procedure UnitTestExportStockRetryOnIdempotencyConcurrentRequest()
+ var
+ Shop: Record "Shpfy Shop";
+ ShopLocation: Record "Shpfy Shop Location";
+ Item: Record Item;
+ ShopifyProduct: Record "Shpfy Product";
+ ShopInventory: Record "Shpfy Shop Inventory";
+ InventorySubscriber: Codeunit "Shpfy Inventory Subscriber";
+ CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
+ InventoryAPI: Codeunit "Shpfy Inventory API";
+ StockCalculate: Enum "Shpfy Stock Calculation";
+ begin
+ // [SCENARIO] Export stock retries on IDEMPOTENCY_CONCURRENT_REQUEST error
+ // [GIVEN] A ShopInventory record with stock different from Shopify stock
+ Initialize();
+
+ Shop := CommunicationMgt.GetShopRecord();
+ CreateShopLocation(ShopLocation, Shop.Code, StockCalculate::"Projected Available Balance Today");
+ CreateItem(Item);
+ UpdateItemInventory(Item, 15);
+ CreateShpfyProduct(ShopifyProduct, ShopInventory, Item.SystemId, Shop.Code, ShopLocation.Id);
+ ShopInventory."Shopify Stock" := 5;
+ ShopInventory.Modify();
+
+ // [GIVEN] The inventory subscriber is configured to fail once with IDEMPOTENCY_CONCURRENT_REQUEST then succeed
+ BindSubscription(InventorySubscriber);
+ InventorySubscriber.SetRetryScenario(Enum::"Shpfy Inventory Retry Scenario"::FailOnceThenSucceed);
+ InventorySubscriber.SetErrorCode('IDEMPOTENCY_CONCURRENT_REQUEST');
+ InventoryAPI.SetShop(Shop.Code);
+
+ // [WHEN] ExportStock is called
+ ShopInventory.SetRange("Shop Code", Shop.Code);
+ ShopInventory.SetRange("Variant Id", ShopInventory."Variant Id");
+ InventoryAPI.ExportStock(ShopInventory);
+
+ // [THEN] The mutation was retried and succeeded (2 calls total)
+ LibraryAssert.AreEqual(2, InventorySubscriber.GetCallCount(), 'Expected 2 GraphQL calls (1 failure + 1 retry success)');
+
+ UnbindSubscription(InventorySubscriber);
+ end;
+
+ [Test]
+ procedure UnitTestExportStockRetryOnChangeFromQuantityStale()
+ var
+ Shop: Record "Shpfy Shop";
+ ShopLocation: Record "Shpfy Shop Location";
+ Item: Record Item;
+ ShopifyProduct: Record "Shpfy Product";
+ ShopInventory: Record "Shpfy Shop Inventory";
+ InventorySubscriber: Codeunit "Shpfy Inventory Subscriber";
+ CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
+ InventoryAPI: Codeunit "Shpfy Inventory API";
+ StockCalculate: Enum "Shpfy Stock Calculation";
+ begin
+ // [SCENARIO] Export stock retries on CHANGE_FROM_QUANTITY_STALE error
+ // [GIVEN] A ShopInventory record with stock different from Shopify stock
+ Initialize();
+
+ Shop := CommunicationMgt.GetShopRecord();
+ CreateShopLocation(ShopLocation, Shop.Code, StockCalculate::"Projected Available Balance Today");
+ CreateItem(Item);
+ UpdateItemInventory(Item, 20);
+ CreateShpfyProduct(ShopifyProduct, ShopInventory, Item.SystemId, Shop.Code, ShopLocation.Id);
+ ShopInventory."Shopify Stock" := 10;
+ ShopInventory.Modify();
+
+ // [GIVEN] The inventory subscriber is configured to fail once with CHANGE_FROM_QUANTITY_STALE then succeed
+ BindSubscription(InventorySubscriber);
+ InventorySubscriber.SetRetryScenario(Enum::"Shpfy Inventory Retry Scenario"::FailOnceThenSucceed);
+ InventorySubscriber.SetErrorCode('CHANGE_FROM_QUANTITY_STALE');
+ InventoryAPI.SetShop(Shop.Code);
+
+ // [WHEN] ExportStock is called
+ ShopInventory.SetRange("Shop Code", Shop.Code);
+ ShopInventory.SetRange("Variant Id", ShopInventory."Variant Id");
+ InventoryAPI.ExportStock(ShopInventory);
+
+ // [THEN] The mutation was retried and succeeded (2 calls total)
+ LibraryAssert.AreEqual(2, InventorySubscriber.GetCallCount(), 'Expected 2 GraphQL calls (1 failure + 1 retry success)');
+
+ UnbindSubscription(InventorySubscriber);
+ end;
+
+ [Test]
+ procedure UnitTestExportStockLogsSkippedRecordAfterMaxRetries()
+ var
+ Shop: Record "Shpfy Shop";
+ ShopLocation: Record "Shpfy Shop Location";
+ Item: Record Item;
+ ShopifyProduct: Record "Shpfy Product";
+ ShopInventory: Record "Shpfy Shop Inventory";
+ SkippedRecord: Record "Shpfy Skipped Record";
+ InventorySubscriber: Codeunit "Shpfy Inventory Subscriber";
+ CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
+ InventoryAPI: Codeunit "Shpfy Inventory API";
+ StockCalculate: Enum "Shpfy Stock Calculation";
+ SkippedCountBefore: Integer;
+ SkippedCountAfter: Integer;
+ begin
+ // [SCENARIO] Export stock logs skipped record when max retries exceeded
+ // [GIVEN] A ShopInventory record with stock different from Shopify stock
+ Initialize();
+
+ Shop := CommunicationMgt.GetShopRecord();
+ CreateShopLocation(ShopLocation, Shop.Code, StockCalculate::"Projected Available Balance Today");
+ CreateItem(Item);
+ UpdateItemInventory(Item, 25);
+ CreateShpfyProduct(ShopifyProduct, ShopInventory, Item.SystemId, Shop.Code, ShopLocation.Id);
+ ShopInventory."Shopify Stock" := 15;
+ ShopInventory.Modify();
+
+ // [GIVEN] Count of skipped records before export
+ SkippedCountBefore := SkippedRecord.Count();
+
+ // [GIVEN] The inventory subscriber is configured to always fail with concurrency error
+ BindSubscription(InventorySubscriber);
+ InventorySubscriber.SetRetryScenario(Enum::"Shpfy Inventory Retry Scenario"::AlwaysFail);
+ InventorySubscriber.SetErrorCode('IDEMPOTENCY_CONCURRENT_REQUEST');
+ InventoryAPI.SetShop(Shop.Code);
+
+ // [WHEN] ExportStock is called
+ ShopInventory.SetRange("Shop Code", Shop.Code);
+ ShopInventory.SetRange("Variant Id", ShopInventory."Variant Id");
+ InventoryAPI.ExportStock(ShopInventory);
+
+ // [THEN] A skipped record was logged
+ SkippedCountAfter := SkippedRecord.Count();
+ LibraryAssert.IsTrue(SkippedCountAfter > SkippedCountBefore, 'Expected a skipped record to be logged after max retries');
+
+ // [THEN] The mutation was retried max times (2 calls: 1 initial + 1 retry)
+ LibraryAssert.AreEqual(2, InventorySubscriber.GetCallCount(), 'Expected 2 GraphQL calls (1 initial + 1 retry)');
+
+ UnbindSubscription(InventorySubscriber);
+ end;
+
+ [Test]
+ procedure UnitTestCalcStockIncludesChangeFromQuantityNull()
+ var
+ Shop: Record "Shpfy Shop";
+ ShopLocation: Record "Shpfy Shop Location";
+ Item: Record Item;
+ ShopifyProduct: Record "Shpfy Product";
+ ShopInventory: Record "Shpfy Shop Inventory";
+ InventorySubscriber: Codeunit "Shpfy Inventory Subscriber";
+ CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
+ InventoryAPI: Codeunit "Shpfy Inventory API";
+ StockCalculate: Enum "Shpfy Stock Calculation";
+ LastGraphQLRequest: Text;
+ begin
+ // [SCENARIO] CalcStock includes changeFromQuantity: null in the GraphQL mutation
+ // [GIVEN] A ShopInventory record with stock different from Shopify stock
+ Initialize();
+
+ Shop := CommunicationMgt.GetShopRecord();
+ CreateShopLocation(ShopLocation, Shop.Code, StockCalculate::"Projected Available Balance Today");
+ CreateItem(Item);
+ UpdateItemInventory(Item, 30);
+ CreateShpfyProduct(ShopifyProduct, ShopInventory, Item.SystemId, Shop.Code, ShopLocation.Id);
+ ShopInventory."Shopify Stock" := 20;
+ ShopInventory.Modify();
+
+ // [GIVEN] The inventory subscriber captures the GraphQL request
+ BindSubscription(InventorySubscriber);
+ InventorySubscriber.SetRetryScenario(Enum::"Shpfy Inventory Retry Scenario"::Success);
+ InventoryAPI.SetShop(Shop.Code);
+
+ // [WHEN] ExportStock is called
+ ShopInventory.SetRange("Shop Code", Shop.Code);
+ ShopInventory.SetRange("Variant Id", ShopInventory."Variant Id");
+ InventoryAPI.ExportStock(ShopInventory);
+
+ // [THEN] The GraphQL request contains changeFromQuantity: null
+ LastGraphQLRequest := InventorySubscriber.GetLastGraphQLRequest();
+ LibraryAssert.IsTrue(LastGraphQLRequest.Contains('"changeFromQuantity":null'), 'Expected changeFromQuantity: null in GraphQL request');
+
+ UnbindSubscription(InventorySubscriber);
+ end;
+
+ [Test]
+ procedure UnitTestIdempotencyKeyIsGenerated()
+ var
+ Shop: Record "Shpfy Shop";
+ ShopLocation: Record "Shpfy Shop Location";
+ Item: Record Item;
+ ShopifyProduct: Record "Shpfy Product";
+ ShopInventory: Record "Shpfy Shop Inventory";
+ InventorySubscriber: Codeunit "Shpfy Inventory Subscriber";
+ CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
+ InventoryAPI: Codeunit "Shpfy Inventory API";
+ StockCalculate: Enum "Shpfy Stock Calculation";
+ LastGraphQLRequest: Text;
+ begin
+ // [SCENARIO] Idempotency key is generated and included in the GraphQL mutation
+ // [GIVEN] A ShopInventory record with stock different from Shopify stock
+ Initialize();
+
+ Shop := CommunicationMgt.GetShopRecord();
+ CreateShopLocation(ShopLocation, Shop.Code, StockCalculate::"Projected Available Balance Today");
+ CreateItem(Item);
+ UpdateItemInventory(Item, 35);
+ CreateShpfyProduct(ShopifyProduct, ShopInventory, Item.SystemId, Shop.Code, ShopLocation.Id);
+ ShopInventory."Shopify Stock" := 25;
+ ShopInventory.Modify();
+
+ // [GIVEN] The inventory subscriber captures the GraphQL request
+ BindSubscription(InventorySubscriber);
+ InventorySubscriber.SetRetryScenario(Enum::"Shpfy Inventory Retry Scenario"::Success);
+ InventoryAPI.SetShop(Shop.Code);
+
+ // [WHEN] ExportStock is called
+ ShopInventory.SetRange("Shop Code", Shop.Code);
+ ShopInventory.SetRange("Variant Id", ShopInventory."Variant Id");
+ InventoryAPI.ExportStock(ShopInventory);
+
+ // [THEN] The GraphQL request contains @idempotent directive with a GUID key
+ LastGraphQLRequest := InventorySubscriber.GetLastGraphQLRequest();
+ LibraryAssert.IsTrue(LastGraphQLRequest.Contains('@idempotent(key:'), 'Expected @idempotent directive in GraphQL request');
+
+ UnbindSubscription(InventorySubscriber);
+ end;
+
+ local procedure CreateItem(var Item: Record Item)
+ begin
+ LibraryInventory.CreateItemWithoutVAT(Item);
+ end;
+
+ local procedure CreateShpfyProduct(var ShopifyProduct: Record "Shpfy Product"; var ShopInventory: Record "Shpfy Shop Inventory"; ItemSystemId: Guid; ShopCode: Code[20]; ShopLocationId: BigInteger)
+ var
+ ShopifyVariant: Record "Shpfy Variant";
+ begin
+ ShopifyProduct.Init();
+ ShopifyProduct.Id := Any.IntegerInRange(10000, 999999);
+ ShopifyProduct."Item SystemId" := ItemSystemId;
+ ShopifyProduct."Shop Code" := ShopCode;
+ ShopifyProduct.Insert();
+
+ ShopifyVariant.Init();
+ ShopifyVariant.Id := Any.IntegerInRange(10000, 999999);
+ ShopifyVariant."Product Id" := ShopifyProduct.Id;
+ ShopifyVariant."Item SystemId" := ItemSystemId;
+ ShopifyVariant."Shop Code" := ShopCode;
+ ShopifyVariant.Insert();
+
+ ShopInventory.Init();
+ ShopInventory."Inventory Item Id" := Any.IntegerInRange(10000, 999999);
+ ShopInventory."Shop Code" := ShopCode;
+ ShopInventory."Location Id" := ShopLocationId;
+ ShopInventory."Product Id" := ShopifyProduct.Id;
+ ShopInventory."Variant Id" := ShopifyVariant.Id;
+ ShopInventory.Insert();
+ end;
+
+ local procedure CreateShopLocation(var ShopLocation: Record "Shpfy Shop Location"; ShopCode: Code[20]; StockCalculation: Enum "Shpfy Stock Calculation")
+ begin
+ ShopLocation.Init();
+ ShopLocation."Shop Code" := ShopCode;
+ ShopLocation.Id := Any.IntegerInRange(10000, 999999);
+ ShopLocation."Stock Calculation" := StockCalculation;
+ ShopLocation.Active := true;
+ ShopLocation."Default Product Location" := true;
+ ShopLocation.Insert();
+ end;
+
+ local procedure UpdateItemInventory(Item: Record Item; Qty: Decimal)
+ var
+ ItemJournalLine: Record "Item Journal Line";
+ begin
+ LibraryInventory.CreateItemJournalLineInItemTemplate(ItemJournalLine, Item."No.", '', '', Qty);
+ LibraryInventory.PostItemJournalLine(ItemJournalLine."Journal Template Name", ItemJournalLine."Journal Batch Name");
+ end;
+}
diff --git a/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventoryRetryScenario.Enum.al b/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventoryRetryScenario.Enum.al
new file mode 100644
index 0000000000..7bc3a3a128
--- /dev/null
+++ b/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventoryRetryScenario.Enum.al
@@ -0,0 +1,28 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Integration.Shopify.Test;
+
+///
+/// Enum Shpfy Inventory Retry Scenario (ID 139617).
+/// Scenarios for simulating inventory API retry behavior in tests.
+///
+enum 139617 "Shpfy Inventory Retry Scenario"
+{
+ Extensible = false;
+
+ value(0; Success)
+ {
+ Caption = 'Success';
+ }
+ value(1; FailOnceThenSucceed)
+ {
+ Caption = 'Fail Once Then Succeed';
+ }
+ value(2; AlwaysFail)
+ {
+ Caption = 'Always Fail';
+ }
+}
diff --git a/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventorySubscriber.Codeunit.al b/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventorySubscriber.Codeunit.al
new file mode 100644
index 0000000000..9ef65637a6
--- /dev/null
+++ b/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventorySubscriber.Codeunit.al
@@ -0,0 +1,103 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Integration.Shopify.Test;
+
+using Microsoft.Integration.Shopify;
+
+///
+/// Codeunit Shpfy Inventory Subscriber (ID 139618).
+/// Mock subscriber for inventory API tests to simulate GraphQL responses.
+///
+codeunit 139618 "Shpfy Inventory Subscriber"
+{
+ SingleInstance = true;
+ EventSubscriberInstance = Manual;
+
+ var
+ RetryScenario: Enum "Shpfy Inventory Retry Scenario";
+ ErrorCode: Text;
+ CallCount: Integer;
+ LastGraphQLRequest: Text;
+
+ internal procedure SetRetryScenario(NewScenario: Enum "Shpfy Inventory Retry Scenario")
+ begin
+ RetryScenario := NewScenario;
+ CallCount := 0;
+ end;
+
+ internal procedure SetErrorCode(NewErrorCode: Text)
+ begin
+ ErrorCode := NewErrorCode;
+ end;
+
+ internal procedure GetCallCount(): Integer
+ begin
+ exit(CallCount);
+ end;
+
+ internal procedure GetLastGraphQLRequest(): Text
+ begin
+ exit(LastGraphQLRequest);
+ end;
+
+ [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
+ Uri: Text;
+ GraphQLQuery: Text;
+ InventorySetQuantitiesGQLTxt: Label 'inventorySetQuantities', 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 begin
+ LastGraphQLRequest := GraphQLQuery;
+ if GraphQLQuery.Contains(InventorySetQuantitiesGQLTxt) then begin
+ CallCount += 1;
+ HttpResponseMessage := GetInventoryResponse();
+ end;
+ end;
+ end;
+ end;
+ end;
+
+ local procedure GetInventoryResponse(): HttpResponseMessage
+ var
+ HttpResponseMessage: HttpResponseMessage;
+ ResponseJson: Text;
+ SuccessResponseTxt: Label '{"data":{"inventorySetQuantities":{"inventoryAdjustmentGroup":{"id":"gid://shopify/InventoryAdjustmentGroup/12345"},"userErrors":[]}}}', Locked = true;
+ ErrorResponseTxt: Label '{"data":{"inventorySetQuantities":{"inventoryAdjustmentGroup":null,"userErrors":[{"field":["input"],"message":"Concurrent request detected","code":"%1"}]}}}', Comment = '%1 = Error code', Locked = true;
+ begin
+ case RetryScenario of
+ RetryScenario::Success:
+ ResponseJson := SuccessResponseTxt;
+ RetryScenario::FailOnceThenSucceed:
+ if CallCount <= 1 then
+ ResponseJson := StrSubstNo(ErrorResponseTxt, ErrorCode)
+ else
+ ResponseJson := SuccessResponseTxt;
+ RetryScenario::AlwaysFail:
+ ResponseJson := StrSubstNo(ErrorResponseTxt, ErrorCode);
+ end;
+
+ HttpResponseMessage.Content.WriteFrom(ResponseJson);
+ exit(HttpResponseMessage);
+ end;
+}
diff --git a/src/Apps/W1/Shopify/Test/Order Refunds/ShpfyOrderRefundsHelper.Codeunit.al b/src/Apps/W1/Shopify/Test/Order Refunds/ShpfyOrderRefundsHelper.Codeunit.al
index 48fd0c0082..7f6e47be8a 100644
--- a/src/Apps/W1/Shopify/Test/Order Refunds/ShpfyOrderRefundsHelper.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Order Refunds/ShpfyOrderRefundsHelper.Codeunit.al
@@ -186,17 +186,17 @@ codeunit 139564 "Shpfy Order Refunds Helper"
exit(ReturnHeader."Return Id");
end;
- internal procedure CreateReturnLine(ReturnOrderId: BigInteger; OrderLineId: BigInteger; ReturnReason: Text): BigInteger
+ internal procedure CreateReturnLine(ReturnOrderId: BigInteger; OrderLineId: BigInteger; ReturnReasonHandle: Text): BigInteger
var
ReturnLine: Record "Shpfy Return Line";
- ReturnEnumConvertor: Codeunit "Shpfy Return Enum Convertor";
begin
ReturnLine."Return Line Id" := Any.IntegerInRange(100000, 999999);
ReturnLine."Return Id" := ReturnOrderId;
ReturnLine.Type := ReturnLine.Type::Default;
ReturnLine."Fulfillment Line Id" := Any.IntegerInRange(100000, 999999);
ReturnLine."Order Line Id" := OrderLineId;
- ReturnLine."Return Reason" := ReturnEnumConvertor.ConvertToReturnReason(ReturnReason);
+ ReturnLine."Return Reason Handle" := CopyStr(ReturnReasonHandle, 1, MaxStrLen(ReturnLine."Return Reason Handle"));
+ ReturnLine."Return Reason Name" := CopyStr(GetReturnReasonNameFromHandle(ReturnReasonHandle), 1, MaxStrLen(ReturnLine."Return Reason Name"));
ReturnLine.Quantity := 1;
ReturnLine."Refundable Quantity" := 0;
ReturnLine."Refunded Quantity" := 1;
@@ -207,19 +207,48 @@ codeunit 139564 "Shpfy Order Refunds Helper"
exit(ReturnLine."Return Line Id");
end;
- internal procedure CreateUnverifiedReturnLine(ReturnId: BigInteger; ReturnReason: Text): BigInteger
+ internal procedure CreateUnverifiedReturnLine(ReturnId: BigInteger; ReturnReasonHandle: Text): BigInteger
var
ReturnLine: Record "Shpfy Return Line";
- ReturnEnumConvertor: Codeunit "Shpfy Return Enum Convertor";
begin
ReturnLine."Return Line Id" := Any.IntegerInRange(100000, 999999);
ReturnLine."Return Id" := ReturnId;
ReturnLine.Type := ReturnLine.Type::Unverified;
- ReturnLine."Return Reason" := ReturnEnumConvertor.ConvertToReturnReason(ReturnReason);
+ ReturnLine."Return Reason Handle" := CopyStr(ReturnReasonHandle, 1, MaxStrLen(ReturnLine."Return Reason Handle"));
+ ReturnLine."Return Reason Name" := CopyStr(GetReturnReasonNameFromHandle(ReturnReasonHandle), 1, MaxStrLen(ReturnLine."Return Reason Name"));
ReturnLine.Quantity := 1;
ReturnLine."Refundable Quantity" := 1;
ReturnLine."Refunded Quantity" := 0;
ReturnLine."Unit Price" := 156.38;
+ ReturnLine.Insert();
+ exit(ReturnLine."Return Line Id");
+ end;
+
+ local procedure GetReturnReasonNameFromHandle(Handle: Text): Text
+ begin
+ // Map handle values to human-readable names (simulating Shopify's returnReasonDefinition)
+ case Handle of
+ 'DEFECTIVE':
+ exit('Defective');
+ 'NOT_AS_DESCRIBED':
+ exit('Not as described');
+ 'WRONG_ITEM':
+ exit('Wrong item');
+ 'SIZE_TOO_SMALL':
+ exit('Size too small');
+ 'SIZE_TOO_LARGE':
+ exit('Size too large');
+ 'STYLE':
+ exit('Style');
+ 'COLOR':
+ exit('Color');
+ 'OTHER':
+ exit('Other');
+ 'UNKNOWN':
+ exit('Unknown');
+ else
+ exit(Handle);
+ end;
end;
internal procedure CreateRefundHeader(OrderId: BigInteger; ReturnId: BigInteger; Amount: Decimal): BigInteger
diff --git a/src/Apps/W1/Shopify/Test/Payments/ShpfyPaymentsTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Payments/ShpfyPaymentsTest.Codeunit.al
index 3e5271e343..860e54c1bf 100644
--- a/src/Apps/W1/Shopify/Test/Payments/ShpfyPaymentsTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Payments/ShpfyPaymentsTest.Codeunit.al
@@ -17,6 +17,62 @@ codeunit 139566 "Shpfy Payments Test"
Any: Codeunit Any;
LibraryAssert: Codeunit "Library Assert";
+ [Test]
+ procedure UnitTestImportPayoutWithExternalTraceId()
+ var
+ Payout: Record "Shpfy Payout";
+ PaymentsAPI: Codeunit "Shpfy Payments API";
+ Id: BigInteger;
+ ExpectedExternalTraceId: Text;
+ JPayout: JsonObject;
+ begin
+ // [SCENARIO] Import payout correctly imports the externalTraceId field (2026-01 API)
+ // [GIVEN] A random Generated Payout with externalTraceId
+ Id := Any.IntegerInRange(10000, 99999);
+ ExpectedExternalTraceId := Any.AlphanumericText(50);
+ JPayout := GetRandomPayout(Id, ExpectedExternalTraceId);
+
+ // [WHEN] Invoke the function ImportPayout(JPayout)
+ PaymentsAPI.ImportPayout(JPayout);
+
+ // [THEN] We must find the "Shpfy Payout" record with the correct externalTraceId
+ LibraryAssert.IsTrue(Payout.Get(Id), 'Get "Shpfy Payout" record');
+ LibraryAssert.AreEqual(ExpectedExternalTraceId, Payout."External Trace Id", 'External Trace Id should match');
+ end;
+
+ local procedure GetRandomPayout(Id: BigInteger; ExternalTraceId: Text): JsonObject
+ var
+ JPayout: JsonObject;
+ JNet: JsonObject;
+ JSummary: JsonObject;
+ JAmount: JsonObject;
+ PayoutGidTxt: Label 'gid://shopify/ShopifyPaymentsPayout/%1', Comment = '%1 = id', Locked = true;
+ begin
+ JPayout.Add('id', StrSubstNo(PayoutGidTxt, Id));
+ JPayout.Add('status', 'SCHEDULED');
+ JPayout.Add('externalTraceId', ExternalTraceId);
+ JPayout.Add('issuedAt', Format(Today, 0, 9));
+ JNet.Add('amount', Any.DecimalInRange(1000, 2));
+ JNet.Add('currencyCode', 'USD');
+ JPayout.Add('net', JNet);
+
+ // Add summary with fee/gross amounts
+ JAmount.Add('amount', 0);
+ JSummary.Add('adjustmentsFee', JAmount);
+ JSummary.Add('adjustmentsGross', JAmount);
+ JSummary.Add('chargesFee', JAmount);
+ JSummary.Add('chargesGross', JAmount);
+ JSummary.Add('refundsFee', JAmount);
+ JSummary.Add('refundsFeeGross', JAmount);
+ JSummary.Add('reservedFundsFee', JAmount);
+ JSummary.Add('reservedFundsGross', JAmount);
+ JSummary.Add('retriedPayoutsFee', JAmount);
+ JSummary.Add('retriedPayoutsGross', JAmount);
+ JPayout.Add('summary', JSummary);
+
+ exit(JPayout);
+ end;
+
[Test]
procedure UnitTestImportPayment()
var
diff --git a/src/Apps/W1/Shopify/Test/Products/ShpfyVariantBatchingTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Products/ShpfyVariantBatchingTest.Codeunit.al
new file mode 100644
index 0000000000..620254673d
--- /dev/null
+++ b/src/Apps/W1/Shopify/Test/Products/ShpfyVariantBatchingTest.Codeunit.al
@@ -0,0 +1,196 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Integration.Shopify.Test;
+
+using Microsoft.Integration.Shopify;
+using System.TestLibraries.Utilities;
+
+///
+/// Codeunit Shpfy Variant Batching Test (ID 139620).
+/// Tests for variant batch size calculations based on 50,000 inventory quantities limit.
+///
+codeunit 139620 "Shpfy Variant Batching Test"
+{
+ Subtype = Test;
+ TestType = IntegrationTest;
+ TestPermissions = Disabled;
+
+ var
+ Shop: Record "Shpfy Shop";
+ Any: Codeunit Any;
+ LibraryAssert: Codeunit "Library Assert";
+ ShpfyInitializeTest: Codeunit "Shpfy Initialize Test";
+ IsInitialized: Boolean;
+
+ local procedure Initialize()
+ var
+ CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
+ begin
+ if IsInitialized then
+ exit;
+ IsInitialized := true;
+ Shop := ShpfyInitializeTest.CreateShop();
+ CommunicationMgt.SetShop(Shop);
+ end;
+
+ [Test]
+ procedure UnitTestGetMaxVariantsPerBatchNoLocations()
+ var
+ ShopLocation: Record "Shpfy Shop Location";
+ VariantAPI: Codeunit "Shpfy Variant API";
+ MaxVariants: Integer;
+ begin
+ // [SCENARIO] GetMaxVariantsPerBatch returns 250 when no active default product locations exist
+ // [GIVEN] No active default product locations for the shop
+ Initialize();
+ ShopLocation.SetRange("Shop Code", Shop.Code);
+ ShopLocation.DeleteAll();
+ VariantAPI.SetShop(Shop);
+
+ // [WHEN] GetMaxVariantsPerBatch is called
+ MaxVariants := VariantAPI.GetMaxVariantsPerBatch();
+
+ // [THEN] Returns 250 (default batch size)
+ LibraryAssert.AreEqual(250, MaxVariants, 'Expected 250 when no inventory locations exist');
+ end;
+
+ [Test]
+ procedure UnitTestGetMaxVariantsPerBatchSingleLocation()
+ var
+ ShopLocation: Record "Shpfy Shop Location";
+ VariantAPI: Codeunit "Shpfy Variant API";
+ MaxVariants: Integer;
+ begin
+ // [SCENARIO] GetMaxVariantsPerBatch returns 50000 when only 1 active default product location exists
+ // [GIVEN] 1 active default product location
+ Initialize();
+ ShopLocation.SetRange("Shop Code", Shop.Code);
+ ShopLocation.DeleteAll();
+ CreateShopLocation(Shop.Code, true, true);
+ VariantAPI.SetShop(Shop);
+
+ // [WHEN] GetMaxVariantsPerBatch is called
+ MaxVariants := VariantAPI.GetMaxVariantsPerBatch();
+
+ // [THEN] Returns 50000 (50000 div 1)
+ LibraryAssert.AreEqual(50000, MaxVariants, 'Expected 50000 when 1 location exists (50000 / 1)');
+ end;
+
+ [Test]
+ procedure UnitTestGetMaxVariantsPerBatchMultipleLocations()
+ var
+ ShopLocation: Record "Shpfy Shop Location";
+ VariantAPI: Codeunit "Shpfy Variant API";
+ MaxVariants: Integer;
+ begin
+ // [SCENARIO] GetMaxVariantsPerBatch returns 50000 div LocationCount when multiple locations exist
+ // [GIVEN] 100 active default product locations
+ Initialize();
+ ShopLocation.SetRange("Shop Code", Shop.Code);
+ ShopLocation.DeleteAll();
+ CreateMultipleShopLocations(Shop.Code, 100);
+ VariantAPI.SetShop(Shop);
+
+ // [WHEN] GetMaxVariantsPerBatch is called
+ MaxVariants := VariantAPI.GetMaxVariantsPerBatch();
+
+ // [THEN] Returns 500 (50000 div 100)
+ LibraryAssert.AreEqual(500, MaxVariants, 'Expected 500 when 100 locations exist (50000 / 100)');
+ end;
+
+ [Test]
+ procedure UnitTestGetMaxVariantsPerBatchHighLocationCount()
+ var
+ ShopLocation: Record "Shpfy Shop Location";
+ VariantAPI: Codeunit "Shpfy Variant API";
+ MaxVariants: Integer;
+ begin
+ // [SCENARIO] GetMaxVariantsPerBatch returns correct value for high location count
+ // [GIVEN] 500 active default product locations
+ Initialize();
+ ShopLocation.SetRange("Shop Code", Shop.Code);
+ ShopLocation.DeleteAll();
+ CreateMultipleShopLocations(Shop.Code, 500);
+ VariantAPI.SetShop(Shop);
+
+ // [WHEN] GetMaxVariantsPerBatch is called
+ MaxVariants := VariantAPI.GetMaxVariantsPerBatch();
+
+ // [THEN] Returns 100 (50000 div 500)
+ LibraryAssert.AreEqual(100, MaxVariants, 'Expected 100 when 500 locations exist (50000 / 500)');
+ end;
+
+ [Test]
+ procedure UnitTestGetDefaultLocationCountNoLocations()
+ var
+ ShopLocation: Record "Shpfy Shop Location";
+ VariantAPI: Codeunit "Shpfy Variant API";
+ LocationCount: Integer;
+ begin
+ // [SCENARIO] GetDefaultLocationCount returns 0 when no active default product locations exist
+ // [GIVEN] No active default product locations for the shop
+ Initialize();
+ ShopLocation.SetRange("Shop Code", Shop.Code);
+ ShopLocation.DeleteAll();
+ VariantAPI.SetShop(Shop);
+
+ // [WHEN] GetDefaultLocationCount is called
+ LocationCount := VariantAPI.GetDefaultLocationCount();
+
+ // [THEN] Returns 0
+ LibraryAssert.AreEqual(0, LocationCount, 'Expected 0 when no default product locations exist');
+ end;
+
+ [Test]
+ procedure UnitTestGetDefaultLocationCountOnlyActiveDefaultLocations()
+ var
+ ShopLocation: Record "Shpfy Shop Location";
+ VariantAPI: Codeunit "Shpfy Variant API";
+ LocationCount: Integer;
+ begin
+ // [SCENARIO] GetDefaultLocationCount only counts active default product locations
+ // [GIVEN] Mix of active/inactive and default/non-default locations
+ Initialize();
+ ShopLocation.SetRange("Shop Code", Shop.Code);
+ ShopLocation.DeleteAll();
+
+ // Active, default product location (should be counted)
+ CreateShopLocation(Shop.Code, true, true);
+ CreateShopLocation(Shop.Code, true, true);
+ // Active, but not default product location (should NOT be counted)
+ CreateShopLocation(Shop.Code, true, false);
+ // Inactive, default product location (should NOT be counted)
+ CreateShopLocation(Shop.Code, false, true);
+
+ VariantAPI.SetShop(Shop);
+
+ // [WHEN] GetDefaultLocationCount is called
+ LocationCount := VariantAPI.GetDefaultLocationCount();
+
+ // [THEN] Returns 2 (only active default product locations)
+ LibraryAssert.AreEqual(2, LocationCount, 'Expected 2 (only active default product locations)');
+ end;
+
+ local procedure CreateShopLocation(ShopCode: Code[20]; Active: Boolean; DefaultProductLocation: Boolean)
+ var
+ ShopLocation: Record "Shpfy Shop Location";
+ begin
+ ShopLocation.Init();
+ ShopLocation."Shop Code" := ShopCode;
+ ShopLocation.Id := Any.IntegerInRange(10000, 9999999);
+ ShopLocation.Active := Active;
+ ShopLocation."Default Product Location" := DefaultProductLocation;
+ ShopLocation.Insert();
+ end;
+
+ local procedure CreateMultipleShopLocations(ShopCode: Code[20]; Count: Integer)
+ var
+ i: Integer;
+ begin
+ for i := 1 to Count do
+ CreateShopLocation(ShopCode, true, true);
+ end;
+}