-
Notifications
You must be signed in to change notification settings - Fork 412
[Shopify] Fix incorrect G/L line on credit memo for refund with exchange item #8554
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f58788b
fdb572d
17360cb
16c7661
7ab9e00
120477b
66d5398
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # cost: 15 | ||
| {"query":"{ order(id: \"gid://shopify/Order/{{OrderId}}\") { returns(first: 10, after: \"{{After}}\") { pageInfo { endCursor hasNextPage } nodes { id exchangeLineItems(first: 250) { nodes { lineItems { id }}}}}}}"} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # cost: 15 | ||
| {"query":"{ order(id: \"gid://shopify/Order/{{OrderId}}\") { returns(first: 10) { pageInfo { endCursor hasNextPage } nodes { id exchangeLineItems(first: 250) { nodes { lineItems { id }}}}}}}"} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # cost: 15 | ||
| {"query":"{ refund(id: \"gid://shopify/Refund/{{RefundId}}\") { return { exchangeLineItems(first: 10, after: \"{{After}}\") { pageInfo { endCursor hasNextPage } nodes { id quantity variantId lineItems { id originalUnitPriceSet { presentmentMoney { amount } shopMoney { amount }} discountedUnitPriceSet { presentmentMoney { amount } shopMoney { amount }} totalDiscountSet { presentmentMoney { amount } shopMoney { amount }} taxLines { priceSet { presentmentMoney { amount } shopMoney { amount }}}}}}}}}"} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # cost: 15 | ||
| {"query":"{ refund(id: \"gid://shopify/Refund/{{RefundId}}\") { return { exchangeLineItems(first: 10) { pageInfo { endCursor hasNextPage } nodes { id quantity variantId lineItems { id originalUnitPriceSet { presentmentMoney { amount } shopMoney { amount }} discountedUnitPriceSet { presentmentMoney { amount } shopMoney { amount }} totalDiscountSet { presentmentMoney { amount } shopMoney { amount }} taxLines { priceSet { presentmentMoney { amount } shopMoney { amount }}}}}}}}}"} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,6 +61,8 @@ codeunit 30228 "Shpfy Refunds API" | |
| ReturnLocations := CollectReturnLocations(RefundHeader."Return Id"); | ||
| GetRefundLines(RefundId, RefundHeader, ReturnLocations); | ||
| GetRefundShippingLines(RefundId); | ||
| if RefundHeader."Return Id" <> 0 then | ||
| GetRefundExchangeLines(RefundId, RefundHeader); | ||
| end; | ||
|
|
||
| local procedure GetRefundHeader(RefundId: BigInteger; UpdatedAt: DateTime; var RefundHeader: Record "Shpfy Refund Header") | ||
|
|
@@ -155,6 +157,43 @@ codeunit 30228 "Shpfy Refunds API" | |
| until not JsonHelper.GetValueAsBoolean(JResponse, 'data.refund.refundShippingLines.pageInfo.hasNextPage'); | ||
| end; | ||
|
|
||
| local procedure GetRefundExchangeLines(RefundId: BigInteger; RefundHeader: Record "Shpfy Refund Header") | ||
| var | ||
| DataCapture: Record "Shpfy Data Capture"; | ||
| GraphQLType: Enum "Shpfy GraphQL Type"; | ||
| Parameters: Dictionary of [Text, Text]; | ||
| JResponse: JsonToken; | ||
| JExchangeLineItems: JsonArray; | ||
| JExchangeLineItem: JsonToken; | ||
| JLineItems: JsonArray; | ||
| JLineItem: JsonToken; | ||
| ExchangeQuantity: Integer; | ||
| LineItemId: BigInteger; | ||
| begin | ||
| Parameters.Add('RefundId', Format(RefundId)); | ||
| GraphQLType := "Shpfy GraphQL Type"::Refunds_GetRefundExchangeLines; | ||
| repeat | ||
| JResponse := CommunicationMgt.ExecuteGraphQL(GraphQLType, Parameters); | ||
| DataCapture.Add(Database::"Shpfy Refund Header", RefundHeader.SystemId, JResponse); | ||
| GraphQLType := "Shpfy GraphQL Type"::Refunds_GetNextRefundExchangeLines; | ||
|
onbuyuka marked this conversation as resolved.
|
||
| JExchangeLineItems := JsonHelper.GetJsonArray(JResponse, 'data.refund.return.exchangeLineItems.nodes'); | ||
| if Parameters.ContainsKey('After') then | ||
| Parameters.Set('After', JsonHelper.GetValueAsText(JResponse, 'data.refund.return.exchangeLineItems.pageInfo.endCursor')) | ||
| else | ||
| Parameters.Add('After', JsonHelper.GetValueAsText(JResponse, 'data.refund.return.exchangeLineItems.pageInfo.endCursor')); | ||
|
|
||
| foreach JExchangeLineItem in JExchangeLineItems do begin | ||
| ExchangeQuantity := JsonHelper.GetValueAsInteger(JExchangeLineItem, 'quantity'); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In ShpfyRefundsAPI.GetRefundExchangeLines, ExchangeQuantity is read once per ExchangeLineItem ('quantity' field) but FillInExchangeRefundLine is invoked once per entry in that same ExchangeLineItem's 'lineItems' array, passing the full ExchangeLineItem-level ExchangeQuantity to every entry.MarkExchangeItemOrderLines (same PR) explicitly loops over multiple 'lineItems' per ExchangeLineItem to collect distinct order-line ids, which shows the author anticipated an ExchangeLineItem can be fulfilled by more than one order line (e.g. split fulfillment) -- yet FillInExchangeRefundLine has no way to know each split's individual share of the quantity, because the GraphQL query fetches 'quantity' only at the ExchangeLineItem level, not per lineItem. When an ExchangeLineItem has more than one lineItems entry, each resulting synthetic 'Shpfy Refund Line' will carry the FULL ExchangeQuantity (and the derived Subtotal/Tax amounts computed from it) instead of a proportional share, double- (or N-times-) counting the exchange offset on the credit memo. The one new test (UnitTestCreateCrMemoFromRefundWithExchangeItem) only exercises a single lineItem per exchange, so this path is unexercised. If the impact were confirmed to be a common real-world scenario this would warrant a major/blocker-level fix (and a corresponding BCQuality knowledge rule); recommend either fetching a per-lineItem quantity from the Shopify API if available, or asserting/handling the single-lineItem case explicitly and logging/erroring on the multi-lineItem case until it is properly split. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one looks make sense, should we consider to improve it? |
||
| JLineItems := JsonHelper.GetJsonArray(JExchangeLineItem, 'lineItems'); | ||
| foreach JLineItem in JLineItems do begin | ||
| LineItemId := CommunicationMgt.GetIdOfGId(JsonHelper.GetValueAsText(JLineItem, 'id')); | ||
| if LineItemId <> 0 then | ||
| FillInExchangeRefundLine(RefundHeader, LineItemId, ExchangeQuantity, JLineItem.AsObject()); | ||
| end; | ||
| end; | ||
| until not JsonHelper.GetValueAsBoolean(JResponse, 'data.refund.return.exchangeLineItems.pageInfo.hasNextPage'); | ||
| end; | ||
|
|
||
| local procedure CollectReturnLocations(ReturnId: BigInteger): Dictionary of [BigInteger, BigInteger] | ||
| var | ||
| ReturnsAPI: Codeunit "Shpfy Returns API"; | ||
|
|
@@ -243,6 +282,66 @@ codeunit 30228 "Shpfy Refunds API" | |
| exit((RefundHeader."Return Id" > 0) or (RefundHeader."Total Refunded Amount" > 0)); | ||
| end; | ||
|
|
||
| internal procedure FillInExchangeRefundLine(RefundHeader: Record "Shpfy Refund Header"; OrderLineId: BigInteger; ExchangeQuantity: Integer; JLineItem: JsonObject) | ||
| var | ||
| DataCapture: Record "Shpfy Data Capture"; | ||
| RefundLine: Record "Shpfy Refund Line"; | ||
| SyntheticRefundLineId: BigInteger; | ||
| UnitPriceShop: Decimal; | ||
| UnitPricePresentment: Decimal; | ||
| TotalDiscountShop: Decimal; | ||
| TotalDiscountPresentment: Decimal; | ||
| SubtotalShop: Decimal; | ||
| SubtotalPresentment: Decimal; | ||
| TotalTaxShop: Decimal; | ||
| TotalTaxPresentment: Decimal; | ||
|
onbuyuka marked this conversation as resolved.
|
||
| JTaxLines: JsonArray; | ||
| JTaxLine: JsonToken; | ||
| begin | ||
| if ExchangeQuantity <= 0 then | ||
| exit; | ||
|
|
||
| // A Shopify line item id is globally unique, so negating it yields a synthetic refund-line id | ||
| // that cannot collide with Shopify's positive refund-line ids or with another exchange line. | ||
| SyntheticRefundLineId := -OrderLineId; | ||
|
|
||
| UnitPriceShop := JsonHelper.GetValueAsDecimal(JLineItem, 'originalUnitPriceSet.shopMoney.amount'); | ||
| UnitPricePresentment := JsonHelper.GetValueAsDecimal(JLineItem, 'originalUnitPriceSet.presentmentMoney.amount'); | ||
| TotalDiscountShop := JsonHelper.GetValueAsDecimal(JLineItem, 'totalDiscountSet.shopMoney.amount'); | ||
| TotalDiscountPresentment := JsonHelper.GetValueAsDecimal(JLineItem, 'totalDiscountSet.presentmentMoney.amount'); | ||
| SubtotalShop := (UnitPriceShop * ExchangeQuantity) - TotalDiscountShop; | ||
| SubtotalPresentment := (UnitPricePresentment * ExchangeQuantity) - TotalDiscountPresentment; | ||
| JTaxLines := JsonHelper.GetJsonArray(JLineItem, 'taxLines'); | ||
| foreach JTaxLine in JTaxLines do begin | ||
| TotalTaxShop += JsonHelper.GetValueAsDecimal(JTaxLine, 'priceSet.shopMoney.amount'); | ||
| TotalTaxPresentment += JsonHelper.GetValueAsDecimal(JTaxLine, 'priceSet.presentmentMoney.amount'); | ||
|
onbuyuka marked this conversation as resolved.
|
||
| end; | ||
|
|
||
| if not RefundLine.Get(RefundHeader."Refund Id", SyntheticRefundLineId) then begin | ||
| RefundLine.Init(); | ||
| RefundLine."Refund Line Id" := SyntheticRefundLineId; | ||
| RefundLine."Refund Id" := RefundHeader."Refund Id"; | ||
| RefundLine."Order Line Id" := OrderLineId; | ||
| RefundLine.Insert(); | ||
| end; | ||
|
|
||
| RefundLine."Restock Type" := RefundLine."Restock Type"::Return; | ||
| RefundLine.Quantity := -ExchangeQuantity; | ||
| RefundLine.Restocked := false; | ||
| RefundLine.Amount := UnitPriceShop; | ||
| RefundLine."Presentment Amount" := UnitPricePresentment; | ||
| RefundLine."Subtotal Amount" := -SubtotalShop; | ||
| RefundLine."Presentment Subtotal Amount" := -SubtotalPresentment; | ||
| RefundLine."Total Tax Amount" := -TotalTaxShop; | ||
| RefundLine."Presentment Total Tax Amount" := -TotalTaxPresentment; | ||
| RefundLine."Can Create Credit Memo" := true; | ||
|
onbuyuka marked this conversation as resolved.
|
||
| RefundLine."Location Id" := 0; | ||
|
onbuyuka marked this conversation as resolved.
|
||
| RefundLine."Is Exchange Item" := true; | ||
| RefundLine.Modify(); | ||
|
|
||
| DataCapture.Add(Database::"Shpfy Refund Line", RefundLine.SystemId, JLineItem); | ||
| end; | ||
|
|
||
| local procedure UpdateTransactions(JRefund: JsonObject; RefundHeader: Record "Shpfy Refund Header") | ||
| var | ||
| OrderTransaction: Record "Shpfy Order Transaction"; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.