[Shopify] Fix incorrect G/L line on credit memo for refund with exchange item#8554
[Shopify] Fix incorrect G/L line on credit memo for refund with exchange item#8554onbuyuka wants to merge 3 commits into
Conversation
…nge item When a Shopify return includes an exchange item (customer returns A, keeps B), the Sales Credit Memo created from the refund contained an incorrect G/L line posted to Shop.Refund Account, which doubled the customer balance impact of the exchange-item value. Symptom from the bug report: invoice 2,258, CR memo 1,528, customer ledger residual 730 (== 2 x 365 exchange value). Root cause - Shopify exposes exchange items on Return.exchangeLineItems, not on Refund.refundLineItems. Refund.totalRefundedSet is already net of the exchange value. So when the connector summed refund lines it found a gap vs totalRefundedSet and FillRemainingAmountLineFields inserted a balancing G/L line for that gap. Fix - Phase A (order): pull Order.returns.exchangeLineItems via a new paginated GraphQL query and flag the matching Shpfy Order Line rows with `Is Exchange Item'' = true. ShpfyProcessOrder skips flagged rows when projecting to BC Sales Lines, so the invoice does not include item B. - Phase B (refund): pull Refund.return.exchangeLineItems via a new paginated GraphQL query and insert one synthetic Shpfy Refund Line per (ExchangeLineItem x lineItem) pair with negative quantity, Restock Type = Return, and prices mirroring the new item. The existing CreateSalesLinesFromRefundLines path then emits a negative-qty Type::Item CR-memo line that offsets the exchange value; totals reconcile to Refund.totalRefundedSet and no spurious G/L line is created. Net effect for the reported scenario - Invoice = 1,893 (A only). - CR memo = +1 A and -1 B totalling 1,528. - Customer ledger residual = 365 (correct). - B leaves BC inventory exactly once via the CR memo negative line, matching what Shopify physically shipped. Notes - The Phase B fetch runs as part of GetRefund, which is called from ImportOrder.SetAndCreateRelatedRecords before InsertOrderLinesAndRelatedRecords. At that point the exchange item's Shpfy Order Line does not exist yet, so the new code stores the OrderLineId directly and lets the FlowField resolve later when the credit memo is built (same pattern as the existing FillInRefundLine). - Only takes effect for orders / refunds (re)imported after deployment; refunds already in DB need re-import to pick up the synthetic exchange refund lines. AB#637828 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace SalesLine.FindFirst() with not SalesLine.IsEmpty() in the two quantity-sign assertions. The returned record is not used, which AA0175 flags as an error in the BCApps ruleset (CodeCop, generalAction = Error). AB#637828 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DataCapture recorded before full line processingIn GetRefundExchangeLines, DataCapture.Add is called immediately after the first ExecuteGraphQL call, before FillInExchangeRefundLine processes the response. If FillInExchangeRefundLine later throws an error, the data capture record already exists, potentially causing duplicate capture entries on retry. Recommendation:
Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
|
Extra GraphQL call for every imported orderMarkExchangeItemOrderLines unconditionally executes at least one GraphQL API call (Orders_GetOrderExchangeLineItems) for every order import, including orders that have never had a return. Because the repeat…until loop fires before checking hasNextPage, there is no zero-call fast path. For high-volume Shopify stores this substantially increases API quota consumption and import duration. Recommendation:
Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
Synthetic refund line ID collision above 1000 line itemsSyntheticExchangeRefundLineId computes -(ExchangeLineItemId * 1000 + LineItemIndex). If any ExchangeLineItem has more than 999 associated line items, or if two ExchangeLineItems differ by exactly 1 and the first has LineItemIndex values reaching into the thousands, the formula produces duplicate IDs. The comment acknowledges "before collision risk" but leaves it silent—a collision would silently overwrite the earlier record during RefundLine.Insert/Modify. Recommendation:
Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why |
- MarkExchangeItemOrderLines: early-exit when the order has no return
(Return Status is blank or No Return), avoiding an extra GraphQL call on
the common no-return import path.
- MarkExchangeItemOrderLines: SetLoadFields("Line Id", "Is Exchange Item")
before the order-line scan so only the needed fields are fetched.
- Synthetic refund-line id: derive from -OrderLineId (a Shopify line item id
is globally unique) instead of -(ExchangeLineItemId * 1000 + index). This
removes the 1000-line-item collision ceiling and drops the helper plus the
index/multiplier bookkeeping.
- ShpfyRefundLines page: hide the internal "Is Exchange Item" column by
default (Visible = false); users can still reveal it via personalization.
AB#637828
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Thanks for the review. Pushed 17360cb addressing the actionable items; rationale for the rest below. Applied
Not changed (with rationale)
|
| OrderLine.SetLoadFields("Line Id", "Is Exchange Item"); | ||
| if OrderLine.FindSet() then | ||
| repeat | ||
| if ExchangeLineIds.Contains(OrderLine."Line Id") and (not OrderLine."Is Exchange Item") then begin |
There was a problem hiding this comment.
Exchange-item flag never cleared on re-import
MarkExchangeItemOrderLines sets "Is Exchange Item" := true for lines identified as exchange items but never sets it back to false for lines that were previously marked as exchange items but whose associated return was subsequently cancelled or removed in Shopify. After a return cancellation, those order lines will remain permanently marked as exchange items, causing them to be incorrectly excluded from BC sales documents on every subsequent re-import.
Recommendation:
- Before re-marking, reset all existing exchange-item flags for the order to
false, then re-apply the flags based on the current API response.
| if ExchangeLineIds.Contains(OrderLine."Line Id") and (not OrderLine."Is Exchange Item") then begin | |
| // Before the ExchangeLineIds loop: | |
| OrderLine.SetRange("Shopify Order Id", OrderHeader."Shopify Order Id"); | |
| OrderLine.SetRange("Is Exchange Item", true); | |
| if OrderLine.FindSet(true) then | |
| repeat | |
| OrderLine."Is Exchange Item" := false; | |
| OrderLine.Modify(); | |
| until OrderLine.Next() = 0; | |
| OrderLine.SetRange("Is Exchange Item"); |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
|
What & why
A Sales Credit Memo created from a Shopify refund contained an incorrect
G/L Accountline posted toShop."Refund Account"whenever the originating return included an exchange item (customer returns item A and keeps a different item B in exchange). The bug doubled the customer-balance impact of the exchange value -- in the reported scenario: invoice 2,258, CR memo 1,528, customer ledger residual 730 == 2 × 365 (where 365 = exchange-item value).Root cause
Shopify exposes exchange items on
Return.exchangeLineItems, not onRefund.refundLineItems, andRefund.totalRefundedSetis already net of the exchange value. The connector summed refund lines (returned items only) againsttotalRefundedSetandFillRemainingAmountLineFieldsinserted a balancing G/L line for the gap.Fix (two phases)
Phase A -- order import
Orders_GetOrderExchangeLineItems/…_Nextthat readorder(id) { returns { exchangeLineItems { lineItems { id } } } }.Shpfy Order Line."Is Exchange Item"Boolean.ShpfyImportOrder.MarkExchangeItemOrderLinesruns after order lines are imported and flags exchange-item rows.ShpfyProcessOrder.CreateLinesFromShopifyOrderandApplyGlobalDiscountsskip flagged rows so the BC sales invoice excludes the exchange item.ConsiderRefundsInQuantityAndAmountsskips flagged rows so the Phase-B negative-qty refund entries don't perturb order-header math.Phase B -- refund import
Refunds_GetRefundExchangeLines/…_Nextthat readrefund(id) { return { exchangeLineItems { nodes { id quantity lineItems { id originalUnitPriceSet { ... } ... } } } } }.Shpfy Refund Line."Is Exchange Item"Boolean.ShpfyRefundsAPI.GetRefundExchangeLinesinserts one syntheticShpfy Refund Lineper(ExchangeLineItem × lineItem)pair: synthetic negativeRefund Line Id(so it can't collide with Shopify-issued positive ids),Quantity = -ExchangeQuantity,Restock Type = Return, prices taken from the new item.CreateSalesLinesFromRefundLinesthen emits a negative-qtyType::ItemCR-memo line that offsets the exchange value -- totals reconcile toRefund.totalRefundedSetwith no balancing G/L line.Net effect for the reported scenario
Linked work
Fixes #
AB#637828
How I validated this
What I tested and the outcome
al_buildclean for bothShopify Connector(App) andShopify Connector Testprojects.al_run_testsagainstShpfy Order Refund Test(139611): 14/14 pass, including the newUnitTestCreateCrMemoFromRefundWithExchangeItemwhich asserts:Type::Itemsales lines on the credit memo with quantities+1and-1;Type::"G/L Account"line pointing atShop."Refund Account";SalesHeader."Amount Including VAT"equalsRefundHeader."Total Refunded Amount".Risk & compatibility
"Is Exchange Item"Boolean fields onShpfy Order Line(id 22) andShpfy Refund Line(id 14). Defaultfalse; existing rows behave unchanged.read_returns/read_ordersalready covered by the connector's scope.Shpfy Refund Header.Updated Atgates the refund header refresh, but my newGetRefundExchangeLinescall runs after that gate and on every fetch).Shpfy Order Subform,Shpfy Refund Lines).