diff --git a/src/Apps/W1/Shopify/App/app.json b/src/Apps/W1/Shopify/App/app.json index a6d665c241..df75bcf1ee 100644 --- a/src/Apps/W1/Shopify/App/app.json +++ b/src/Apps/W1/Shopify/App/app.json @@ -17,7 +17,7 @@ "idRanges": [ { "from": 30100, - "to": 30450 + "to": 30460 } ], "internalsVisibleTo": [ 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 c4f451dea9..1b112cdeb1 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 cf867bfacf..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(10); - 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 7c671bb64c..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 ea5ae95ea3..9cc8fb62e9 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 { ... on ReturnLineItem { id quantity returnReason returnReasonNote refundableQuantity refundedQuantity customerNote 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 454f137960..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 } } 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 8edaca2ecc..c764188a7f 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 { ... on ReturnLineItem { id quantity returnReason returnReasonNote refundableQuantity refundedQuantity customerNote 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 82d4c56201..caee267ffe 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 644499e6cc..e8c769cab4 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 @@ -300,11 +300,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..da354bbffd 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 @@ -24,11 +24,6 @@ codeunit 30195 "Shpfy Inventory API" JsonHelper: Codeunit "Shpfy Json Helper"; InventoryIds: List of [Guid]; - /// - /// Get Stock. - /// - /// Parameter of type Record "Shopify Shop Inventory". - /// Return variable "Stock" of type Decimal. internal procedure GetStock(ShopInventory: Record "Shpfy Shop Inventory") Stock: Decimal var Item: Record Item; @@ -78,11 +73,6 @@ codeunit 30195 "Shpfy Inventory API" end; end; - /// - /// Get Id. - /// - /// Parameter of type JsonObject. - /// Return variable "Result" of type BigInteger. local procedure GetId(JObject: JsonObject) Result: BigInteger var JValue: JsonValue; @@ -97,12 +87,6 @@ codeunit 30195 "Shpfy Inventory API" end; end; - /// - /// Get Inventory Levels. - /// - /// Parameter of type JsonObject. - /// Parameter of type JsonObject. - /// Return value of type Boolean. local procedure GetInventoryLevels(JObject: JsonObject; var JResult: JsonObject): Boolean; var JData: JsonObject; @@ -141,44 +125,96 @@ 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()); + if InputSize > 0 then + 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 := 3; + 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,22 +232,19 @@ 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; end; - /// - /// Has Next Results. - /// - /// Parameter of type JsonObject. - /// Return value of type Boolean. local procedure HasNextResults(JObject: JsonObject): Boolean var JPageInfo: JsonObject; @@ -293,10 +326,6 @@ codeunit 30195 "Shpfy Inventory API" end; end; - /// - /// Import Stock. - /// - /// Parameter of type Record "Shopify Shop Location". internal procedure ImportStock(ShopLocation: Record "Shpfy Shop Location") var Parameters: Dictionary of [Text, Text]; @@ -313,10 +342,6 @@ codeunit 30195 "Shpfy Inventory API" until not HasNextResults(JInventoryLevels); end; - /// - /// Set Shop. - /// - /// Parameter of type Code[20]. internal procedure SetShop(ShopCode: Code[20]) begin if ShopifyShop.Code <> ShopCode then begin 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 Return Refund Processing/Codeunits/ShpfyCreateSalesDocRefund.Codeunit.al b/src/Apps/W1/Shopify/App/src/Order Return Refund Processing/Codeunits/ShpfyCreateSalesDocRefund.Codeunit.al index f4ee09ff69..20b14d631b 100644 --- a/src/Apps/W1/Shopify/App/src/Order Return Refund Processing/Codeunits/ShpfyCreateSalesDocRefund.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Order Return Refund Processing/Codeunits/ShpfyCreateSalesDocRefund.Codeunit.al @@ -172,6 +172,7 @@ codeunit 30246 "Shpfy Create Sales Doc. Refund" else if RefundHeader."Return Id" > 0 then begin ReturnLine.SetRange("Return Id", RefundHeader."Return Id"); + ReturnLine.SetRange(Type, ReturnLine.Type::Default); ReturnLine.SetAutoCalcFields("Item No.", "Variant Code", Description, "Unit of Measure Code"); if ReturnLine.FindSet(false) then CreateSalesLinesFromReturnLines(ReturnLine, RefundHeader, SalesHeader, LineNo); diff --git a/src/Apps/W1/Shopify/App/src/Order Returns/Codeunits/ShpfyReturnEnumConvertor.Codeunit.al b/src/Apps/W1/Shopify/App/src/Order Returns/Codeunits/ShpfyReturnEnumConvertor.Codeunit.al index ac154ac6ed..c2ac41b1d6 100644 --- a/src/Apps/W1/Shopify/App/src/Order Returns/Codeunits/ShpfyReturnEnumConvertor.Codeunit.al +++ b/src/Apps/W1/Shopify/App/src/Order Returns/Codeunits/ShpfyReturnEnumConvertor.Codeunit.al @@ -13,7 +13,6 @@ codeunit 30224 "Shpfy Return Enum Convertor" var ReturnDeclineReasons: Dictionary of [Text, Enum "Shpfy Return Decline Reason"]; ReturnStatuses: Dictionary of [Text, Enum "Shpfy Return Status"]; - ReturnReasons: Dictionary of [Text, Enum "Shpfy Return Reason"]; #region "Shpfy Return Decline Reason" local procedure FillInReturnDeclineReasons() @@ -37,11 +36,6 @@ codeunit 30224 "Shpfy Return Enum Convertor" if ReturnDeclineReasons.ContainsKey(Value) then exit(ReturnDeclineReasons.Get(Value)); end; - - internal procedure ConvertToText(Value: Enum "Shpfy Return Decline Reason"): text - begin - exit(Value.Names.Get(Value.Ordinals().IndexOf(Value.AsInteger())).Trim().ToUpper().Replace(' ', '_')); - end; #endregion "Shpfy Return Decline Reason" #region "Shpfy Return Status" @@ -66,39 +60,5 @@ codeunit 30224 "Shpfy Return Enum Convertor" if ReturnStatuses.ContainsKey(Value) then exit(ReturnStatuses.Get(Value)); end; - - internal procedure ConvertToText(Value: Enum "Shpfy Return Status"): text - begin - exit(Value.Names.Get(Value.Ordinals().IndexOf(Value.AsInteger())).Trim().ToUpper().Replace(' ', '_')); - end; #endregion "Shpfy Return Status" - - #region "Shpfy Return Reason" - local procedure FillInReturnReasons() - var - EnumValues: List of [Integer]; - EnumNames: List of [Text]; - Index: Integer; - begin - if ReturnReasons.Count > 0 then - exit; - - EnumValues := Enum::"Shpfy Return Reason".Ordinals(); - EnumNames := Enum::"Shpfy Return Reason".Names(); - for Index := 1 to EnumValues.Count do - ReturnReasons.Add(EnumNames.Get(Index).Trim().ToUpper().Replace(' ', '_'), Enum::"Shpfy Return Reason".FromInteger(EnumValues.Get(Index))); - end; - - internal procedure ConvertToReturnReason(Value: Text): Enum "Shpfy Return Reason" - begin - FillInReturnReasons(); - if ReturnReasons.ContainsKey(Value) then - exit(ReturnReasons.Get(Value)); - end; - - internal procedure ConvertToText(Value: Enum "Shpfy Return Reason"): text - begin - exit(Value.Names.Get(Value.Ordinals().IndexOf(Value.AsInteger())).Trim().ToUpper().Replace(' ', '_')); - end; - #endregion "Shpfy Return Decline Reason" } \ 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 59062881b2..f7a55b1dd8 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 @@ -57,8 +57,12 @@ codeunit 30250 "Shpfy Returns API" LineParameters.Set('After', JsonHelper.GetValueAsText(JResponse, 'data.return.returnLineItems.pageInfo.endCursor')) else LineParameters.Add('After', JsonHelper.GetValueAsText(JResponse, 'data.return.returnLineItems.pageInfo.endCursor')); - foreach JLine in JLines do - FillInReturnLine(ReturnId, JLine.AsObject(), ReturnLocations); + foreach JLine in JLines do begin + if JsonHelper.GetValueAsText(JLine, '__typename') = 'ReturnLineItem' then + FillInReturnLine(ReturnId, JLine.AsObject(), ReturnLocations); + if JsonHelper.GetValueAsText(JLine, '__typename') = 'UnverifiedReturnLineItem' then + FillInUnverifiedReturnLine(ReturnId, JLine.AsObject()); + end; until not JsonHelper.GetValueAsBoolean(JResponse, 'data.return.returnLineItems.pageInfo.hasNextPage'); end; @@ -115,10 +119,10 @@ codeunit 30250 "Shpfy Returns API" GraphQLType := "Shpfy GraphQL Type"::GetNextReverseFulfillmentOrders; JOrders := JsonHelper.GetJsonArray(JResponse, 'data.return.reverseFulfillmentOrders.nodes'); - if Parameters.ContainsKey('After') then - Parameters.Set('After', JsonHelper.GetValueAsText(JResponse, 'data.return.reverseFulfillmentOrders.pageInfo.endCursor')) + if LineParameters.ContainsKey('After') then + LineParameters.Set('After', JsonHelper.GetValueAsText(JResponse, 'data.return.reverseFulfillmentOrders.pageInfo.endCursor')) else - Parameters.Add('After', JsonHelper.GetValueAsText(JResponse, 'data.return.reverseFulfillmentOrders.pageInfo.endCursor')); + LineParameters.Add('After', JsonHelper.GetValueAsText(JResponse, 'data.return.reverseFulfillmentOrders.pageInfo.endCursor')); foreach JOrder in JOrders do GetReturnLocationsFromReturnFulfillOrder(JsonHelper.GetValueAsText(JOrder, 'id'), ReturnLocations); @@ -140,10 +144,10 @@ codeunit 30250 "Shpfy Returns API" GraphQLType := "Shpfy GraphQL Type"::GetNextReverseFulfillmentOrders; JLines := JsonHelper.GetJsonArray(JResponse, 'data.reverseFulfillmentOrder.lineItems.nodes'); - if Parameters.ContainsKey('After') then - Parameters.Set('After', JsonHelper.GetValueAsText(JResponse, 'data.reverseFulfillmentOrder.lineItems.pageInfo.endCursor')) + if LineParameters.ContainsKey('After') then + LineParameters.Set('After', JsonHelper.GetValueAsText(JResponse, 'data.reverseFulfillmentOrder.lineItems.pageInfo.endCursor')) else - Parameters.Add('After', JsonHelper.GetValueAsText(JResponse, 'data.reverseFulfillmentOrder.lineItems.pageInfo.endCursor')); + LineParameters.Add('After', JsonHelper.GetValueAsText(JResponse, 'data.reverseFulfillmentOrder.lineItems.pageInfo.endCursor')); foreach JLine in JLines do CollectLocationsFromLineDispositions(JLine, ReturnLocations); @@ -163,7 +167,7 @@ codeunit 30250 "Shpfy Returns API" if Dispositions.Count = 0 then exit; - // If dispositions have different locations (Item was restocked to multiple locations), + // If dispositions have different locations (Item was restocked to multiple locations), // we cannot determine the return location for the line Dispositions.Get(0, Disposition); LocationId := JsonHelper.GetValueAsBigInteger(Disposition, 'location.legacyResourceId'); @@ -195,11 +199,13 @@ codeunit 30250 "Shpfy Returns API" if not ReturnLine.Get(Id) then begin ReturnLine."Return Line Id" := Id; ReturnLine."Return Id" := ReturnId; + ReturnLine.Type := ReturnLine.Type::Default; ReturnLine."Fulfillment Line Id" := CommunicationMgt.GetIdOfGId(JsonHelper.GetValueAsText(JLine, 'fulfillmentLineItem.id')); 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; @@ -219,4 +225,34 @@ codeunit 30250 "Shpfy Returns API" ReturnLineRecordRef.Close(); DataCapture.Add(Database::"Shpfy Return Line", ReturnLine.SystemId, JLine); end; + + local procedure FillInUnverifiedReturnLine(ReturnId: BigInteger; JLine: JsonObject) + var + DataCapture: Record "Shpfy Data Capture"; + ReturnLine: Record "Shpfy Return Line"; + ReturnLineRecordRef: RecordRef; + Id: BigInteger; + begin + Id := CommunicationMgt.GetIdOfGId(JsonHelper.GetValueAsText(JLine, 'id')); + if not ReturnLine.Get(Id) then begin + ReturnLine."Return Line Id" := Id; + ReturnLine."Return Id" := ReturnId; + ReturnLine.Type := ReturnLine.Type::Unverified; + ReturnLine.Insert(); + end; + 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')); + + ReturnLineRecordRef.GetTable(ReturnLine); + JsonHelper.GetValueIntoField(JLine, 'quantity', ReturnLineRecordRef, ReturnLine.FieldNo(Quantity)); + JsonHelper.GetValueIntoField(JLine, 'refundableQuantity', ReturnLineRecordRef, ReturnLine.FieldNo("Refundable Quantity")); + JsonHelper.GetValueIntoField(JLine, 'refundedQuantity', ReturnLineRecordRef, ReturnLine.FieldNo("Refunded Quantity")); + JsonHelper.GetValueIntoField(JLine, 'unitPrice.amount', ReturnLineRecordRef, ReturnLine.FieldNo("Unit Price")); + JsonHelper.GetValueIntoField(JLine, 'unitPrice.currency', ReturnLineRecordRef, ReturnLine.FieldNo("Unit Price Currency")); + ReturnLineRecordRef.Modify(); + ReturnLineRecordRef.Close(); + DataCapture.Add(Database::"Shpfy Return Line", ReturnLine.SystemId, JLine); + end; } \ No newline at end of file diff --git a/src/Apps/W1/Shopify/App/src/Order Returns/Enums/ShpfyReturnLineType.Enum.al b/src/Apps/W1/Shopify/App/src/Order Returns/Enums/ShpfyReturnLineType.Enum.al new file mode 100644 index 0000000000..cfaa343332 --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Order Returns/Enums/ShpfyReturnLineType.Enum.al @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------------------------------ +// 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; + +enum 30170 "Shpfy Return Line Type" +{ + value(0; Default) + { + Caption = 'Default'; + } + value(1; Unverified) + { + Caption = 'Unverified'; + } +} \ No newline at end of file diff --git a/src/Apps/W1/Shopify/App/src/Order Returns/Enums/ShpfyReturnReason.Enum.al b/src/Apps/W1/Shopify/App/src/Order Returns/Enums/ShpfyReturnReason.Enum.al index 6ccd9af01d..43411cf2c9 100644 --- a/src/Apps/W1/Shopify/App/src/Order Returns/Enums/ShpfyReturnReason.Enum.al +++ b/src/Apps/W1/Shopify/App/src/Order Returns/Enums/ShpfyReturnReason.Enum.al @@ -7,7 +7,6 @@ namespace Microsoft.Integration.Shopify; enum 30138 "Shpfy Return Reason" { - value(0; " ") { Caption = ' '; 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 f4dd4fd96a..60bd3e9424 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 @@ -17,6 +17,11 @@ page 30149 "Shpfy Return Lines" { repeater(General) { + field(Type; Rec.Type) + { + ApplicationArea = All; + ToolTip = 'Specifies the type of return line.'; + } field("Item No."; Rec."Item No.") { ApplicationArea = All; @@ -38,6 +43,12 @@ page 30149 "Shpfy Return Lines" ToolTip = 'Specifies the quantity being returned.'; } field("Return Reason"; Rec."Return Reason") + { + ApplicationArea = All; + ToolTip = 'Specifies the reason for returning the item.'; + Visible = false; + } + field("Return Reason Name"; Rec."Return Reason Name") { ApplicationArea = All; ToolTip = 'Specifies the reason for returning the item.'; @@ -67,6 +78,11 @@ page 30149 "Shpfy Return Lines" ApplicationArea = All; ToolTip = 'Specifies the unit of measurement.'; } + field("Unit Price"; Rec."Unit Price") + { + ApplicationArea = All; + ToolTip = 'Specifies the price of a single unit of the item.'; + } } group(ReturnReason) { 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 95a2f5861e..66f5e1142c 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 @@ -49,7 +49,7 @@ table 30141 "Shpfy Return Line" } field(6; "Return Reason"; Enum "Shpfy Return Reason") { - Caption = 'Return Reason'; + Caption = 'Return Reason (deprecated)'; DataClassification = SystemMetadata; Editable = false; } @@ -104,6 +104,38 @@ table 30141 "Shpfy Return Line" Caption = 'Customer Note'; DataClassification = SystemMetadata; } + field(15; Type; Enum "Shpfy Return Line Type") + { + Caption = 'Type'; + DataClassification = SystemMetadata; + Editable = false; + } + field(16; "Unit Price"; Decimal) + { + Caption = 'Unit Price'; + DataClassification = SystemMetadata; + Editable = false; + AutoFormatType = 1; + AutoFormatExpression = "Unit Price Currency"; + } + field(17; "Unit Price Currency"; Code[10]) + { + Caption = 'Unit Price Currency'; + 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 8991768583..8b19c46edc 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 @@ -143,6 +143,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 1551727ea9..9fc1f0b9af 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; + } field(101; "Shop Code"; Code[20]) { Caption = 'Shop Code'; 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 d64ce82b40..bea2a0c626 100644 --- a/src/Apps/W1/Shopify/App/src/PermissionSets/ShpfyObjects.PermissionSet.al +++ b/src/Apps/W1/Shopify/App/src/PermissionSets/ShpfyObjects.PermissionSet.al @@ -155,7 +155,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 dfa49a87fa..54ef9deaab 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 @@ -359,7 +359,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"; @@ -416,7 +415,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 0b51046ec7..280984bd90 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 @@ -97,11 +97,15 @@ codeunit 30189 "Shpfy Variant API" JResponse: JsonToken; JVariants: JsonArray; ReturnQuery: Text; + LocationCount: Integer; + InventoryQuantitiesCount: Integer; begin ReturnQuery := ']) {productVariants {legacyResourceId, createdAt, updatedAt}, userErrors {field, message}}}"}'; if ShopifyVariant.FindSet() then begin InventoryQuantities := GetInventoryQuantities(); + LocationCount := GetLocationCount(); + InventoryQuantitiesCount := 0; GraphQuery.Append('{"query":"mutation { productVariantsBulkCreate(productId: \"gid://shopify/Product/'); GraphQuery.Append(Format(ProductId)); GraphQuery.Append('\", strategy: '); @@ -110,10 +114,12 @@ codeunit 30189 "Shpfy Variant API" 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 + (InventoryQuantitiesCount + LocationCount <= GetInventoryQuantitiesLimit()) then begin GraphQuery.Append(VariantGraphQuery.ToText() + ', '); TempNewShopifyVariant := ShopifyVariant; TempNewShopifyVariant.Insert(); + InventoryQuantitiesCount += LocationCount; end else begin GraphQuery.Remove(GraphQuery.Length - 1, 2); GraphQuery.Append(ReturnQuery); @@ -131,6 +137,7 @@ codeunit 30189 "Shpfy Variant API" GraphQuery.Append(Format(Strategy)); GraphQuery.Append(', variants: ['); GraphQuery.Append(VariantGraphQuery.ToText() + ', '); + InventoryQuantitiesCount := LocationCount; end; until ShopifyVariant.Next() = 0; GraphQuery.Remove(GraphQuery.Length - 1, 2); @@ -164,12 +171,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: \"'); @@ -234,11 +235,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)); @@ -321,6 +317,21 @@ codeunit 30189 "Shpfy Variant API" exit(GraphQuery.ToText()); end; + local procedure GetLocationCount(): 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; + + local procedure GetInventoryQuantitiesLimit(): Integer + begin + exit(50000); + end; + local procedure CreateNewVariant(JVariant: JsonToken; var ShopifyVariant: Record "Shpfy Variant"; ProductId: BigInteger): Boolean var NewShopifyVariant: Record "Shpfy Variant"; @@ -730,7 +741,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/BulkOperationCompletedResult.txt b/src/Apps/W1/Shopify/Test/.resources/Bulk Operations/BulkOperationCompletedResult.txt index e9121aaef4..e38c8a6501 100644 --- a/src/Apps/W1/Shopify/Test/.resources/Bulk Operations/BulkOperationCompletedResult.txt +++ b/src/Apps/W1/Shopify/Test/.resources/Bulk Operations/BulkOperationCompletedResult.txt @@ -1 +1 @@ -{ "data": { "node": { "status": "COMPLETED", "errorCode": null, "completedAt": "2021-01-28T19:11:09Z", "url": "", "partialDataUrl": null } }, "extensions": { "cost": { "requestedQueryCost": 1, "actualQueryCost": 1 } } } \ No newline at end of file +{ "data": { "node": { "status": "%1", "errorCode": null, "completedAt": "2021-01-28T19:11:09Z", "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/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..a9e95100f1 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,13 +72,8 @@ 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(); + HttpResponseMessage := GetBulkOperation(); end; end; 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; @@ -162,7 +132,7 @@ codeunit 139615 "Shpfy Bulk Op. Subscriber" exit(HttpResponseMessage); end; - local procedure GetBulkOperationCompletedResult(): HttpResponseMessage + local procedure GetBulkOperation(): HttpResponseMessage var HttpResponseMessage: HttpResponseMessage; Body: Text; @@ -170,6 +140,10 @@ codeunit 139615 "Shpfy Bulk Op. Subscriber" begin NavApp.GetResource('Bulk Operations/BulkOperationCompletedResult.txt', ResInStream, TextEncoding::UTF8); ResInStream.ReadText(Body); + if BulkOperationRunning then + Body := StrSubstNo(Body, 'RUNNING') + else + Body := StrSubstNo(Body, 'COMPLETED'); HttpResponseMessage.Content.WriteFrom(Body); exit(HttpResponseMessage); end; 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 4adbbd82b9..475a4be7b2 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 @@ -47,7 +47,7 @@ codeunit 139633 "Shpfy Bulk Operations Test" ShopifyVariant: Record "Shpfy Variant"; begin BulkOperation.DeleteAll(); - BulkOpSubscriber.SetBulkOperationRunning(false); + // BulkOpSubscriber.SetBulkOperationRunning(false); BulkOpSubscriber.SetBulkUploadFail(false); ShopifyVariant.DeleteAll(); end; 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..da16f8e172 --- /dev/null +++ b/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventoryExportTest.Codeunit.al @@ -0,0 +1,370 @@ +// ------------------------------------------------------------------------------------------------ +// 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 139501). +/// Tests for inventory export functionality including idempotency and retry logic. +/// +codeunit 139594 "Shpfy Inventory Export Test" +{ + Subtype = Test; + TestType = IntegrationTest; + TestPermissions = Disabled; + + var + Any: Codeunit Any; + LibraryAssert: Codeunit "Library Assert"; + LibraryInventory: Codeunit "Library - Inventory"; + IsInitialized: Boolean; + NextId: BigInteger; + + 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); + CreateShopifyProduct(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); + CreateShopifyProduct(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); + CreateShopifyProduct(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); + CreateShopifyProduct(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 (4 calls: 1 initial + 3 retry) + LibraryAssert.AreEqual(4, InventorySubscriber.GetCallCount(), 'Expected 4 GraphQL calls (1 initial + 3 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); + CreateShopifyProduct(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); + CreateShopifyProduct(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 CreateShopifyProduct(var ShopifyProduct: Record "Shpfy Product"; var ShopInventory: Record "Shpfy Shop Inventory"; ItemSystemId: Guid; ShopCode: Code[20]; ShopLocationId: BigInteger) + var + ShopifyVariant: Record "Shpfy Variant"; + ProductId: BigInteger; + VariantId: BigInteger; + InventoryItemId: BigInteger; + begin + ProductId := GetNextId(); + VariantId := GetNextId(); + InventoryItemId := GetNextId(); + + ShopifyProduct.Init(); + ShopifyProduct.Id := ProductId; + ShopifyProduct."Item SystemId" := ItemSystemId; + ShopifyProduct."Shop Code" := ShopCode; + ShopifyProduct.Insert(); + + ShopifyVariant.Init(); + ShopifyVariant.Id := VariantId; + ShopifyVariant."Product Id" := ShopifyProduct.Id; + ShopifyVariant."Item SystemId" := ItemSystemId; + ShopifyVariant."Shop Code" := ShopCode; + ShopifyVariant.Insert(); + + ShopInventory.Init(); + ShopInventory."Inventory Item Id" := InventoryItemId; + ShopInventory."Shop Code" := ShopCode; + ShopInventory."Location Id" := ShopLocationId; + ShopInventory."Product Id" := ShopifyProduct.Id; + ShopInventory."Variant Id" := ShopifyVariant.Id; + ShopInventory.Insert(); + end; + + local procedure GetNextId(): BigInteger + begin + NextId += 1; + exit(NextId); + end; + + local procedure CreateShopLocation(var ShopLocation: Record "Shpfy Shop Location"; ShopCode: Code[20]; StockCalculation: Enum "Shpfy Stock Calculation") + begin + ShopLocation.SetRange("Shop Code", ShopCode); + ShopLocation.SetRange(Active, true); + if ShopLocation.FindFirst() then begin + ShopLocation."Stock Calculation" := StockCalculation; + ShopLocation."Default Product Location" := true; + ShopLocation.Modify(); + exit; + end; + + 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..b942c6f134 --- /dev/null +++ b/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventorySubscriber.Codeunit.al @@ -0,0 +1,104 @@ +// ------------------------------------------------------------------------------------------------ +// 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 139593). +/// Mock subscriber for inventory API tests to simulate GraphQL responses. +/// +codeunit 139593 "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; + LastGraphQLRequest := ''; + 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/ShpfyOrderRefundTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Order Refunds/ShpfyOrderRefundTest.Codeunit.al index 3f2359b693..a21e7a7c66 100644 --- a/src/Apps/W1/Shopify/Test/Order Refunds/ShpfyOrderRefundTest.Codeunit.al +++ b/src/Apps/W1/Shopify/Test/Order Refunds/ShpfyOrderRefundTest.Codeunit.al @@ -466,6 +466,7 @@ codeunit 139611 "Shpfy Order Refund Test" OrderRefundsHelper.SetDefaultSeed(); ReturnId := OrderRefundsHelper.CreateReturn(OrderId); OrderRefundsHelper.CreateReturnLine(ReturnId, OrderId, ''); + OrderRefundsHelper.CreateUnverifiedReturnLine(ReturnId, ''); end; local procedure CreateLocation(var Location: Record Location) 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 f68b72c226..ef86b01aa4 100644 --- a/src/Apps/W1/Shopify/Test/Order Refunds/ShpfyOrderRefundsHelper.Codeunit.al +++ b/src/Apps/W1/Shopify/Test/Order Refunds/ShpfyOrderRefundsHelper.Codeunit.al @@ -43,10 +43,12 @@ codeunit 139564 "Shpfy Order Refunds Helper" ReturnId := CreateReturn(OrderId); CreateReturnLine(ReturnId, ShopifyIds.Get('OrderLine').Get(1), 'DEFECTIVE'); + CreateUnverifiedReturnLine(ReturnId, 'DEFECTIVE'); ShopifyIds.Get('Return').Add(ReturnId); ReturnId := CreateReturn(OrderId); CreateReturnLine(ReturnId, ShopifyIds.Get('OrderLine').Get(2), 'NOT_AS_DESCRIBED'); + CreateUnverifiedReturnLine(ReturnId, 'NOT_AS_DESCRIBED'); ShopifyIds.Get('Return').Add(ReturnId); RefundId := CreateRefundHeader(OrderId, ShopifyIds.Get('Return').Get(1), 156.38); @@ -184,16 +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; @@ -204,6 +207,50 @@ codeunit 139564 "Shpfy Order Refunds Helper" exit(ReturnLine."Return Line Id"); end; + internal procedure CreateUnverifiedReturnLine(ReturnId: BigInteger; ReturnReasonHandle: Text): BigInteger + var + ReturnLine: Record "Shpfy Return Line"; + begin + ReturnLine."Return Line Id" := Any.IntegerInRange(100000, 999999); + ReturnLine."Return Id" := ReturnId; + ReturnLine.Type := ReturnLine.Type::Unverified; + 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 var RefundHeader: Record "Shpfy Refund Header"; diff --git a/src/Apps/W1/Shopify/Test/Payments/ShpfyPaymentsTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Payments/ShpfyPaymentsTest.Codeunit.al index 4112e94df3..155e3ba3a1 100644 --- a/src/Apps/W1/Shopify/Test/Payments/ShpfyPaymentsTest.Codeunit.al +++ b/src/Apps/W1/Shopify/Test/Payments/ShpfyPaymentsTest.Codeunit.al @@ -31,6 +31,33 @@ codeunit 139566 "Shpfy Payments Test" isInitialized := true; end; + [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) + Initialize(); + + // [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.SetShop(Shop); + PaymentsAPI.ImportPayout(JPayout); + + // [THEN] We must find the "Shpfy Payout" record with the correct externalTraceId and Shop Code + LibraryAssert.IsTrue(Payout.Get(Id), 'Get "Shpfy Payout" record'); + LibraryAssert.AreEqual(ExpectedExternalTraceId, Payout."External Trace Id", 'External Trace Id should match'); + LibraryAssert.AreEqual(Shop.Code, Payout."Shop Code", 'Shop Code should match'); + end; + [Test] procedure UnitTestImportPayoutBackfillsShopCode() var @@ -44,7 +71,7 @@ codeunit 139566 "Shpfy Payments Test" // [GIVEN] An existing payout record imported without a shop context (blank Shop Code) Id := Any.IntegerInRange(10000, 99999); - JPayout := GetRandomPayout(Id); + JPayout := GetRandomPayout(Id, Any.AlphanumericText(50)); PaymentsAPI.ImportPayout(JPayout); LibraryAssert.IsTrue(Payout.Get(Id), 'Payout should be created'); LibraryAssert.AreEqual('', Payout."Shop Code", 'Shop Code should initially be blank'); @@ -58,7 +85,7 @@ codeunit 139566 "Shpfy Payments Test" LibraryAssert.AreEqual(Shop.Code, Payout."Shop Code", 'Shop Code should be backfilled on existing payout'); end; - local procedure GetRandomPayout(Id: BigInteger): JsonObject + local procedure GetRandomPayout(Id: BigInteger; ExternalTraceId: Text): JsonObject var JPayout: JsonObject; JNet: JsonObject; @@ -68,11 +95,12 @@ codeunit 139566 "Shpfy Payments Test" 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); @@ -86,7 +114,7 @@ codeunit 139566 "Shpfy Payments Test" JSummary.Add('retriedPayoutsFee', JAmount); JSummary.Add('retriedPayoutsGross', JAmount); JPayout.Add('summary', JSummary); - + exit(JPayout); end; diff --git a/src/Apps/W1/Shopify/Test/app.json b/src/Apps/W1/Shopify/Test/app.json index d627290a69..055702a85a 100644 --- a/src/Apps/W1/Shopify/Test/app.json +++ b/src/Apps/W1/Shopify/Test/app.json @@ -57,7 +57,7 @@ "to": 134247 }, { - "from": 139539, + "from": 139537, "to": 139549 }, { @@ -72,6 +72,10 @@ "from": 139576, "to": 139589 }, + { + "from": 139593, + "to": 139594 + }, { "from": 139601, "to": 139609