From bb2cb80b8606cc584bbdb3c3b2c9e9921a0251cc Mon Sep 17 00:00:00 2001 From: Onat Buyukakkus <55088871+onbuyuka@users.noreply.github.com> Date: Wed, 13 May 2026 13:43:41 +0200 Subject: [PATCH] [Shopify] Fix bulk variant price update sending compareAtPrice as "0" (#7949) Backport of bug #633535 / PR #7949 to releases/27.x. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ShpfyBulkUpdateProductPrice.Codeunit.al | 5 +- .../Codeunits/ShpfyVariantAPI.Codeunit.al | 6 +- .../ShpfyBulkOperationsTest.Codeunit.al | 148 ++++++++++++++++++ 3 files changed, 154 insertions(+), 5 deletions(-) diff --git a/src/Apps/W1/Shopify/App/src/Bulk Operations/Codeunits/ShpfyBulkUpdateProductPrice.Codeunit.al b/src/Apps/W1/Shopify/App/src/Bulk Operations/Codeunits/ShpfyBulkUpdateProductPrice.Codeunit.al index c52e331bfb..7f9ccad662 100644 --- a/src/Apps/W1/Shopify/App/src/Bulk Operations/Codeunits/ShpfyBulkUpdateProductPrice.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Bulk Operations/Codeunits/ShpfyBulkUpdateProductPrice.Codeunit.al @@ -21,7 +21,10 @@ codeunit 30281 "Shpfy Bulk UpdateProductPrice" implements "Shpfy IBulk Operation procedure GetInput(): Text begin - exit('{ "productId": "gid://shopify/Product/%1", "variants": [{ "id": "gid://shopify/ProductVariant/%2", "price": "%3", "compareAtPrice": "%4" }]}'); + // %4 carries the entire optional ', "compareAtPrice": ' fragment (or empty + // string to omit the field, in which case Shopify preserves the existing value). + // It must include the leading comma and field name when present. + exit('{ "productId": "gid://shopify/Product/%1", "variants": [{ "id": "gid://shopify/ProductVariant/%2", "price": "%3"%4 }]}'); end; procedure GetName(): Text[250] 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 6a9ca05393..0b51046ec7 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 @@ -651,11 +651,11 @@ codeunit 30189 "Shpfy Variant API" GraphQuery.Append(Format(ShopifyVariant."Compare at Price", 0, 9)); GraphQuery.Append('\"'); if IsBulkOperationEnabled then - CompareAtPrice := Format(ShopifyVariant."Compare at Price", 0, 9); + CompareAtPrice := ', "compareAtPrice": "' + Format(ShopifyVariant."Compare at Price", 0, 9) + '"'; end else begin HasChange := true; GraphQuery.Append(', compareAtPrice: null'); - CompareAtPrice := '0'; + CompareAtPrice := ', "compareAtPrice": null'; end; GraphQuery.Append('}]) {productVariants {updatedAt}, userErrors {field, message}}}"}'); @@ -665,8 +665,6 @@ codeunit 30189 "Shpfy Variant API" IBulkOperation := BulkOperationType::UpdateProductPrice; if Price = '' then Price := Format(ShopifyVariant.Price, 0, 9); - if CompareAtPrice = '' then - CompareAtPrice := Format(ShopifyVariant."Compare at Price", 0, 9); GraphQueryList.Add(ShopifyVariant.Id, GraphQuery); JRequest.Add('id', ShopifyVariant.Id); diff --git a/src/Apps/W1/Shopify/Test/Bulk Operations/Codeunits/ShpfyBulkOperationsTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Bulk Operations/Codeunits/ShpfyBulkOperationsTest.Codeunit.al index 0d9379aa60..4adbbd82b9 100644 --- a/src/Apps/W1/Shopify/Test/Bulk Operations/Codeunits/ShpfyBulkOperationsTest.Codeunit.al +++ b/src/Apps/W1/Shopify/Test/Bulk Operations/Codeunits/ShpfyBulkOperationsTest.Codeunit.al @@ -296,6 +296,154 @@ codeunit 139633 "Shpfy Bulk Operations Test" ClearSetup(); end; + [Test] + procedure TestBulkUpdateProductPriceClearsCompareAtPriceAsNull() + var + ShopifyVariant: Record "Shpfy Variant"; + xShopifyVariant: Record "Shpfy Variant"; + VariantApi: Codeunit "Shpfy Variant API"; + BulkOperationInput: TextBuilder; + GraphQueryList: Dictionary of [BigInteger, TextBuilder]; + JRequestData: JsonArray; + Jsonl: Text; + begin + // [SCENARIO] Bug fix for ICM 21000001004461: when Compare at Price is cleared (set to 0) + // on the bulk path, the JSONL must contain "compareAtPrice": null, not "compareAtPrice": "0". + Initialize(); + ClearSetup(); + + // [GIVEN] A Shopify variant whose Compare at Price has just been cleared (200 -> 0) + ShopifyVariant.Init(); + ShopifyVariant."Product Id" := Any.IntegerInRange(100000, 555555); + ShopifyVariant.Id := Any.IntegerInRange(100000, 555555); + ShopifyVariant.Price := 100; + ShopifyVariant."Compare at Price" := 0; + ShopifyVariant."Unit Cost" := 50; + ShopifyVariant.Insert(); + xShopifyVariant := ShopifyVariant; + xShopifyVariant."Compare at Price" := 200; + + // [WHEN] UpdateProductPrice runs on the bulk path (RecordCount >= 100) + VariantApi.UpdateProductPrice(ShopifyVariant, xShopifyVariant, BulkOperationInput, GraphQueryList, 100, JRequestData); + + // [THEN] The JSONL line clears Compare at Price with the literal token null, not the string "0" + Jsonl := BulkOperationInput.ToText(); + LibraryAssert.IsTrue(Jsonl.Contains('"compareAtPrice": null'), 'JSONL must clear Compare at Price with null. Was: ' + Jsonl); + LibraryAssert.IsFalse(Jsonl.Contains('"compareAtPrice": "0"'), 'JSONL must not send Compare at Price as the string "0". Was: ' + Jsonl); + LibraryAssert.IsFalse(Jsonl.Contains('"compareAtPrice": "null"'), 'JSONL must not quote the null token. Was: ' + Jsonl); + ClearSetup(); + end; + + [Test] + procedure TestBulkUpdateProductPriceOmitsUnchangedCompareAtPrice() + var + ShopifyVariant: Record "Shpfy Variant"; + xShopifyVariant: Record "Shpfy Variant"; + VariantApi: Codeunit "Shpfy Variant API"; + BulkOperationInput: TextBuilder; + GraphQueryList: Dictionary of [BigInteger, TextBuilder]; + JRequestData: JsonArray; + Jsonl: Text; + begin + // [SCENARIO] Bug fix for ICM 21000001004461: when only Price changes and Compare at Price + // is unchanged in BC, the bulk path must omit compareAtPrice from the JSONL so that + // Shopify preserves whatever value is currently set on the variant. + Initialize(); + ClearSetup(); + + // [GIVEN] A Shopify variant with Compare at Price = 0 (unchanged), Price changing 80 -> 100 + ShopifyVariant.Init(); + ShopifyVariant."Product Id" := Any.IntegerInRange(100000, 555555); + ShopifyVariant.Id := Any.IntegerInRange(100000, 555555); + ShopifyVariant.Price := 100; + ShopifyVariant."Compare at Price" := 0; + ShopifyVariant."Unit Cost" := 50; + ShopifyVariant.Insert(); + xShopifyVariant := ShopifyVariant; + xShopifyVariant.Price := 80; + + // [WHEN] UpdateProductPrice runs on the bulk path + VariantApi.UpdateProductPrice(ShopifyVariant, xShopifyVariant, BulkOperationInput, GraphQueryList, 100, JRequestData); + + // [THEN] compareAtPrice is not in the JSONL at all - Shopify preserves its existing value + Jsonl := BulkOperationInput.ToText(); + LibraryAssert.IsFalse(Jsonl.Contains('compareAtPrice'), 'JSONL must omit compareAtPrice when unchanged in BC. Was: ' + Jsonl); + ClearSetup(); + end; + + [Test] + procedure TestBulkUpdateProductPriceSendsValidCompareAtPriceAsQuoted() + var + ShopifyVariant: Record "Shpfy Variant"; + xShopifyVariant: Record "Shpfy Variant"; + VariantApi: Codeunit "Shpfy Variant API"; + BulkOperationInput: TextBuilder; + GraphQueryList: Dictionary of [BigInteger, TextBuilder]; + JRequestData: JsonArray; + Jsonl: Text; + begin + // [SCENARIO] When Compare at Price is set above Price (a valid sale price), the bulk path + // must send the value as a quoted decimal string, matching the non-bulk GraphQL path. + Initialize(); + ClearSetup(); + + // [GIVEN] Variant going on sale: Price 100, Compare at Price changing 0 -> 200 + ShopifyVariant.Init(); + ShopifyVariant."Product Id" := Any.IntegerInRange(100000, 555555); + ShopifyVariant.Id := Any.IntegerInRange(100000, 555555); + ShopifyVariant.Price := 100; + ShopifyVariant."Compare at Price" := 200; + ShopifyVariant."Unit Cost" := 50; + ShopifyVariant.Insert(); + xShopifyVariant := ShopifyVariant; + xShopifyVariant."Compare at Price" := 0; + + // [WHEN] UpdateProductPrice runs on the bulk path + VariantApi.UpdateProductPrice(ShopifyVariant, xShopifyVariant, BulkOperationInput, GraphQueryList, 100, JRequestData); + + // [THEN] Compare at Price is sent as a quoted decimal string + Jsonl := BulkOperationInput.ToText(); + LibraryAssert.IsTrue(Jsonl.Contains('"compareAtPrice": "200'), 'JSONL must send positive Compare at Price as a quoted decimal. Was: ' + Jsonl); + LibraryAssert.IsFalse(Jsonl.Contains('"compareAtPrice": null'), 'JSONL must not null out a valid Compare at Price. Was: ' + Jsonl); + ClearSetup(); + end; + + [Test] + procedure TestBulkUpdateProductPriceOmitsUnchangedPositiveCompareAtPrice() + var + ShopifyVariant: Record "Shpfy Variant"; + xShopifyVariant: Record "Shpfy Variant"; + VariantApi: Codeunit "Shpfy Variant API"; + BulkOperationInput: TextBuilder; + GraphQueryList: Dictionary of [BigInteger, TextBuilder]; + JRequestData: JsonArray; + Jsonl: Text; + begin + // [SCENARIO] When only Unit Cost changes and Compare at Price is unchanged (even if + // currently > Price), the bulk path must omit compareAtPrice so Shopify preserves it. + Initialize(); + ClearSetup(); + + // [GIVEN] Variant with valid sale (Compare 200 > Price 100); only Unit Cost changes + ShopifyVariant.Init(); + ShopifyVariant."Product Id" := Any.IntegerInRange(100000, 555555); + ShopifyVariant.Id := Any.IntegerInRange(100000, 555555); + ShopifyVariant.Price := 100; + ShopifyVariant."Compare at Price" := 200; + ShopifyVariant."Unit Cost" := 75; + ShopifyVariant.Insert(); + xShopifyVariant := ShopifyVariant; + xShopifyVariant."Unit Cost" := 50; + + // [WHEN] UpdateProductPrice runs on the bulk path + VariantApi.UpdateProductPrice(ShopifyVariant, xShopifyVariant, BulkOperationInput, GraphQueryList, 100, JRequestData); + + // [THEN] compareAtPrice is omitted entirely - Shopify keeps its current value + Jsonl := BulkOperationInput.ToText(); + LibraryAssert.IsFalse(Jsonl.Contains('compareAtPrice'), 'JSONL must omit unchanged compareAtPrice. Was: ' + Jsonl); + ClearSetup(); + end; + local procedure CreateBulkOperation(BulkOperationId: BigInteger; BulkOperationType: Enum "Shpfy Bulk Operation Type"; ShopCode: Code[20]; BulkOperationUrl: Text; RequestData: JsonArray): Record "Shpfy Bulk Operation" var BulkOperation: Record "Shpfy Bulk Operation";