Skip to content

Commit 03c2b5c

Browse files
authored
[Shopify] Improve synchronization with targeted updates for payment transactions and payouts (#6085)
### Summary This PR improves the Shopify Payments synchronization logic by implementing targeted GraphQL queries to update payment transaction payout IDs and payout statuses in batches, rather than relying on the previous approach that coupled transaction imports with payout imports. ### Changes #### New GraphQL Codeunits - **ShpfyGQLPaymTransByIds** (ID 30421): New GraphQL query to retrieve payment transactions by ID filter, returning transaction IDs with their associated payout IDs - **ShpfyGQLPayoutsByIds** (ID 30420): New GraphQL query to retrieve payouts by ID filter, returning payout IDs with their statuses #### GraphQL Type Enum Extension - Added new enum values `GetPaymTransByIds` (145) and `GetPayoutsByIds` (146) to ShpfyGraphQLType.Enum.al with implementations pointing to the new codeunits #### Payments Codeunit Refactoring - **ShpfyPayments.Codeunit.al**: - Renamed `SyncPaymentTransactions` to `SyncPayouts` for clarity - Added new procedures: `UpdatePaymentTransactionPayoutIds` and `UpdatePendingPayouts` - Implemented batch processing (up to 200 at a time) for updating transactions without payout IDs and payouts with pending statuses - Organized code into `#region Payouts` and `#region Disputes` sections #### Payments API Enhancements - **ShpfyPaymentsAPI.Codeunit.al**: - Added `UpdatePaymentTransactionPayoutIds(IdFilter: Text)` - queries Shopify for transactions by ID and updates their payout IDs - Added `UpdatePayouts(IdFilter: Text)` - queries Shopify for payouts by ID and updates their statuses - Simplified `ImportPaymentTransactions` signature (removed var parameter) - Simplified `ImportPaymentTransaction` to handle both new record creation and existing record updates - Added region organization for better code structure #### Report Update - **ShpfySyncPayments.Report.al**: Updated to call the renamed `SyncPayouts` procedure #### Permission Set Update - **ShpfyObjects.PermissionSet.al**: Added execute permissions for the two new GraphQL codeunits #### Test Updates - **ShpfyPaymentsTest.Codeunit.al**: Updated test to use the simplified `ImportPaymentTransaction` signature ### Benefits 1. **Targeted Updates**: Payment transactions without payout IDs and pending payouts are now updated independently using efficient batch queries 2. **Better Performance**: Batch processing with `first: 200` reduces API calls when updating multiple records 3. **Cleaner Code**: Reorganized with regions and simplified method signatures 4. **Maintainability**: Separated concerns between importing new data and updating existing records #### Work Item(s) <!-- Add the issue number here after the #. The issue needs to be open and approved. Submitting PRs with no linked issues or unapproved issues is highly discouraged. --> Fixes [AB#617855](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/617855)
1 parent 8d7e9e4 commit 03c2b5c

File tree

8 files changed

+221
-77
lines changed

8 files changed

+221
-77
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// ------------------------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// Licensed under the MIT License. See License.txt in the project root for license information.
4+
// ------------------------------------------------------------------------------------------------
5+
6+
namespace Microsoft.Integration.Shopify;
7+
8+
codeunit 30421 "Shpfy GQL PaymTransByIds" implements "Shpfy IGraphQL"
9+
{
10+
Access = Internal;
11+
12+
procedure GetGraphQL(): Text
13+
begin
14+
exit('{"query":"{ shopifyPaymentsAccount { balanceTransactions(first: 200, query: \"id:{{IdFilter}}\") { nodes { id associatedPayout { id } } } } }"}');
15+
end;
16+
17+
procedure GetExpectedCost(): Integer
18+
begin
19+
exit(23);
20+
end;
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// ------------------------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// Licensed under the MIT License. See License.txt in the project root for license information.
4+
// ------------------------------------------------------------------------------------------------
5+
6+
namespace Microsoft.Integration.Shopify;
7+
8+
codeunit 30420 "Shpfy GQL PayoutsByIds" implements "Shpfy IGraphQL"
9+
{
10+
Access = Internal;
11+
12+
procedure GetGraphQL(): Text
13+
begin
14+
exit('{"query":"{ shopifyPaymentsAccount { payouts(first: 200, query: \"id:{{IdFilter}}\") { nodes { id status } } } }"}');
15+
end;
16+
17+
procedure GetExpectedCost(): Integer
18+
begin
19+
exit(13);
20+
end;
21+
}

src/Apps/W1/Shopify/App/src/GraphQL/Enums/ShpfyGraphQLType.Enum.al

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,4 +725,15 @@ enum 30111 "Shpfy GraphQL Type" implements "Shpfy IGraphQL"
725725
Caption = 'Set Variant Image';
726726
Implementation = "Shpfy IGraphQL" = "Shpfy GQL SetVariantImage";
727727
}
728+
729+
value(145; GetPaymTransByIds)
730+
{
731+
Caption = 'Get Payment Transactions By Ids';
732+
Implementation = "Shpfy IGraphQL" = "Shpfy GQL PaymTransByIds";
733+
}
734+
value(146; GetPayoutsByIds)
735+
{
736+
Caption = 'Get Payouts By Ids';
737+
Implementation = "Shpfy IGraphQL" = "Shpfy GQL PayoutsByIds";
738+
}
728739
}

src/Apps/W1/Shopify/App/src/Payments/Codeunits/ShpfyPayments.Codeunit.al

Lines changed: 75 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,86 @@ codeunit 30169 "Shpfy Payments"
2222
PaymentsAPI.SetShop(Shop);
2323
end;
2424

25-
internal procedure SyncPaymentTransactions()
25+
#region Payouts
26+
internal procedure SyncPayouts()
27+
begin
28+
UpdatePaymentTransactionPayoutIds();
29+
UpdatePendingPayouts();
30+
ImportNewPaymentTransactions();
31+
ImportNewPayouts();
32+
end;
33+
34+
local procedure ImportNewPaymentTransactions()
2635
var
36+
PaymentTransaction: Record "Shpfy Payment Transaction";
2737
SinceId: BigInteger;
2838
begin
29-
SinceId := GetLastTransactionPayoutId(Shop.Code);
39+
PaymentTransaction.SetRange("Shop Code", Shop.Code);
40+
if PaymentTransaction.FindLast() then
41+
SinceId := PaymentTransaction.Id;
3042
PaymentsAPI.ImportPaymentTransactions(SinceId);
31-
if SinceId > 0 then
32-
ImportPayouts(SinceId - 1);
3343
end;
3444

45+
local procedure ImportNewPayouts()
46+
var
47+
Payout: Record "Shpfy Payout";
48+
SinceId: BigInteger;
49+
begin
50+
if Payout.FindLast() then
51+
SinceId := Payout.Id;
52+
PaymentsAPI.ImportPayouts(SinceId);
53+
end;
54+
55+
local procedure UpdatePaymentTransactionPayoutIds()
56+
var
57+
PaymentTransaction: Record "Shpfy Payment Transaction";
58+
PaymentTransactionIdFilter: Text;
59+
PaymentTransactionCount: Integer;
60+
begin
61+
PaymentTransaction.SetRange("Payout Id", 0);
62+
if PaymentTransaction.FindSet() then
63+
repeat
64+
if PaymentTransactionIdFilter <> '' then
65+
PaymentTransactionIdFilter += ' OR ';
66+
PaymentTransactionIdFilter += Format(PaymentTransaction.Id);
67+
PaymentTransactionCount += 1;
68+
if PaymentTransactionCount = 200 then begin
69+
PaymentsAPI.UpdatePaymentTransactionPayoutIds(PaymentTransactionIdFilter);
70+
PaymentTransactionIdFilter := '';
71+
PaymentTransactionCount := 0;
72+
end;
73+
until PaymentTransaction.Next() = 0;
74+
75+
if PaymentTransactionIdFilter <> '' then
76+
PaymentsAPI.UpdatePaymentTransactionPayoutIds(PaymentTransactionIdFilter);
77+
end;
78+
79+
local procedure UpdatePendingPayouts()
80+
var
81+
Payout: Record "Shpfy Payout";
82+
PayoutIdFilter: Text;
83+
PayoutCount: Integer;
84+
begin
85+
Payout.SetFilter(Status, '<>%1&<>%2', "Shpfy Payout Status"::Paid, "Shpfy Payout Status"::Canceled);
86+
if Payout.FindSet() then
87+
repeat
88+
if PayoutIdFilter <> '' then
89+
PayoutIdFilter += ' OR ';
90+
PayoutIdFilter += Format(Payout.Id);
91+
PayoutCount += 1;
92+
if PayoutCount = 200 then begin
93+
PaymentsAPI.UpdatePayoutStatuses(PayoutIdFilter);
94+
PayoutIdFilter := '';
95+
PayoutCount := 0;
96+
end;
97+
until Payout.Next() = 0;
98+
99+
if PayoutIdFilter <> '' then
100+
PaymentsAPI.UpdatePayoutStatuses(PayoutIdFilter);
101+
end;
102+
#endregion
103+
104+
#region Disputes
35105
internal procedure SyncDisputes()
36106
begin
37107
UpdateUnfinishedDisputes();
@@ -58,26 +128,5 @@ codeunit 30169 "Shpfy Payments"
58128
SinceId := Dispute.Id;
59129
PaymentsAPI.ImportDisputes(SinceId);
60130
end;
61-
62-
local procedure ImportPayouts(SinceId: BigInteger)
63-
var
64-
Payout: Record "Shpfy Payout";
65-
Math: Codeunit "Shpfy Math";
66-
begin
67-
Payout.SetFilter(Status, '<>%1&<>%2', "Shpfy Payout Status"::Paid, "Shpfy Payout Status"::Canceled);
68-
Payout.SetLoadFields(Id);
69-
if Payout.FindFirst() then
70-
SinceId := Math.Min(SinceId, Payout.Id);
71-
72-
PaymentsAPI.ImportPayouts(SinceId);
73-
end;
74-
75-
local procedure GetLastTransactionPayoutId(ShopCode: Code[20]): BigInteger
76-
var
77-
PaymentTransaction: Record "Shpfy Payment Transaction";
78-
begin
79-
PaymentTransaction.SetRange("Shop Code", ShopCode);
80-
if PaymentTransaction.FindLast() then
81-
exit(PaymentTransaction."Payout Id");
82-
end;
131+
#endregion
83132
}

src/Apps/W1/Shopify/App/src/Payments/Codeunits/ShpfyPaymentsAPI.Codeunit.al

Lines changed: 88 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ codeunit 30385 "Shpfy Payments API"
1717
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
1818
JsonHelper: Codeunit "Shpfy Json Helper";
1919

20-
internal procedure ImportPaymentTransactions(var SinceId: BigInteger)
20+
#region Payouts
21+
internal procedure ImportPaymentTransactions(SinceId: BigInteger)
2122
var
2223
GraphQLType: Enum "Shpfy GraphQL Type";
2324
JTransactions: JsonArray;
@@ -30,15 +31,14 @@ codeunit 30385 "Shpfy Payments API"
3031
begin
3132
GraphQLType := GraphQLType::GetPaymentTransactions;
3233
Parameters.Add('SinceId', Format(SinceId));
33-
Clear(SinceId);
3434
repeat
3535
JResponse := CommunicationMgt.ExecuteGraphQL(GraphQLType, Parameters);
3636
if JsonHelper.GetJsonObject(JResponse, JPaymentsAccount, 'data.shopifyPaymentsAccount') then
3737
if JsonHelper.GetJsonArray(JResponse, JTransactions, 'data.shopifyPaymentsAccount.balanceTransactions.edges') then begin
3838
foreach JItem in JTransactions do begin
3939
Cursor := JsonHelper.GetValueAsText(JItem.AsObject(), 'cursor');
4040
if JsonHelper.GetJsonObject(JItem.AsObject(), JNode, 'node') then
41-
ImportPaymentTransaction(JNode, SinceId);
41+
ImportPaymentTransaction(JNode);
4242
end;
4343
if Parameters.ContainsKey('After') then
4444
Parameters.Set('After', Cursor)
@@ -49,19 +49,18 @@ codeunit 30385 "Shpfy Payments API"
4949
until not JsonHelper.GetValueAsBoolean(JResponse, 'data.shopifyPaymentsAccount.balanceTransactions.pageInfo.hasNextPage');
5050
end;
5151

52-
internal procedure ImportPaymentTransaction(JTransaction: JsonObject; var SinceId: BigInteger)
52+
internal procedure ImportPaymentTransaction(JTransaction: JsonObject)
5353
var
5454
DataCapture: Record "Shpfy Data Capture";
5555
PaymentTransaction: Record "Shpfy Payment Transaction";
56-
Math: Codeunit "Shpfy Math";
5756
RecordRef: RecordRef;
5857
Id: BigInteger;
59-
PayoutId: BigInteger;
6058
begin
6159
Id := CommunicationMgt.GetIdOfGId(JsonHelper.GetValueAsText(JTransaction, 'id'));
62-
Clear(PaymentTransaction);
63-
PaymentTransaction.SetRange(Id, Id);
64-
if PaymentTransaction.IsEmpty then begin
60+
if PaymentTransaction.Get(Id) then begin
61+
PaymentTransaction."Payout Id" := CommunicationMgt.GetIdOfGId(JsonHelper.GetValueAsText(JTransaction, 'associatedPayout.id'));
62+
PaymentTransaction.Modify();
63+
end else begin
6564
RecordRef.Open(Database::"Shpfy Payment Transaction");
6665
RecordRef.Init();
6766
JsonHelper.GetValueIntoField(JTransaction, 'test', RecordRef, PaymentTransaction.FieldNo(Test));
@@ -82,20 +81,6 @@ codeunit 30385 "Shpfy Payments API"
8281
PaymentTransaction."Payout Id" := CommunicationMgt.GetIdOfGId(JsonHelper.GetValueAsText(JTransaction, 'associatedPayout.id'));
8382
PaymentTransaction.Insert();
8483
DataCapture.Add(Database::"Shpfy Payment Transaction", PaymentTransaction.SystemId, JTransaction);
85-
if SinceId = 0 then
86-
SinceId := PaymentTransaction."Payout Id"
87-
else
88-
if PaymentTransaction."Payout Id" > 0 then
89-
SinceId := Math.Min(SinceId, PaymentTransaction."Payout Id");
90-
end else begin
91-
PaymentTransaction.Get(Id);
92-
if PaymentTransaction."Payout Id" = 0 then begin
93-
PayoutId := CommunicationMgt.GetIdOfGId(JsonHelper.GetValueAsText(JTransaction, 'associatedPayout.id'));
94-
if PayoutId <> 0 then begin
95-
PaymentTransaction."Payout Id" := PayoutId;
96-
PaymentTransaction.Modify();
97-
end;
98-
end;
9984
end;
10085
end;
10186

@@ -166,6 +151,79 @@ codeunit 30385 "Shpfy Payments API"
166151
DataCapture.Add(Database::"Shpfy Payout", Payout.SystemId, JPayout);
167152
end;
168153

154+
internal procedure UpdatePaymentTransactionPayoutIds(IdFilter: Text)
155+
var
156+
PaymentTransaction: Record "Shpfy Payment Transaction";
157+
GraphQLType: Enum "Shpfy GraphQL Type";
158+
JTransactions: JsonArray;
159+
JPaymentsAccount: JsonObject;
160+
JNode: JsonToken;
161+
JResponse: JsonToken;
162+
Parameters: Dictionary of [Text, Text];
163+
Id: BigInteger;
164+
PayoutId: BigInteger;
165+
begin
166+
GraphQLType := GraphQLType::GetPaymTransByIds;
167+
Parameters.Add('IdFilter', IdFilter);
168+
JResponse := CommunicationMgt.ExecuteGraphQL(GraphQLType, Parameters);
169+
if JsonHelper.GetJsonObject(JResponse, JPaymentsAccount, 'data.shopifyPaymentsAccount') then
170+
if JsonHelper.GetJsonArray(JResponse, JTransactions, 'data.shopifyPaymentsAccount.balanceTransactions.nodes') then
171+
foreach JNode in JTransactions do begin
172+
Id := CommunicationMgt.GetIdOfGId(JsonHelper.GetValueAsText(JNode.AsObject(), 'id'));
173+
PayoutId := CommunicationMgt.GetIdOfGId(JsonHelper.GetValueAsText(JNode.AsObject(), 'associatedPayout.id'));
174+
if PaymentTransaction.Get(Id) then
175+
if PaymentTransaction."Payout Id" <> PayoutId then begin
176+
PaymentTransaction."Payout Id" := PayoutId;
177+
PaymentTransaction.Modify();
178+
end;
179+
end;
180+
end;
181+
182+
internal procedure UpdatePayoutStatuses(IdFilter: Text)
183+
var
184+
Payout: Record "Shpfy Payout";
185+
GraphQLType: Enum "Shpfy GraphQL Type";
186+
JPayouts: JsonArray;
187+
JPaymentsAccount: JsonObject;
188+
JNode: JsonToken;
189+
JResponse: JsonToken;
190+
Parameters: Dictionary of [Text, Text];
191+
Id: BigInteger;
192+
begin
193+
GraphQLType := GraphQLType::GetPayoutsByIds;
194+
Parameters.Add('IdFilter', IdFilter);
195+
JResponse := CommunicationMgt.ExecuteGraphQL(GraphQLType, Parameters);
196+
if JsonHelper.GetJsonObject(JResponse, JPaymentsAccount, 'data.shopifyPaymentsAccount') then
197+
if JsonHelper.GetJsonArray(JResponse, JPayouts, 'data.shopifyPaymentsAccount.payouts.nodes') then
198+
foreach JNode in JPayouts do begin
199+
Id := CommunicationMgt.GetIdOfGId(JsonHelper.GetValueAsText(JNode.AsObject(), 'id'));
200+
if Payout.Get(Id) then begin
201+
Payout.Status := ConvertToPayoutStatus(JsonHelper.GetValueAsText(JNode.AsObject(), 'status'));
202+
Payout.Modify();
203+
end;
204+
end;
205+
end;
206+
207+
local procedure ConvertToPayoutStatus(Value: Text): Enum "Shpfy Payout Status"
208+
begin
209+
Value := CommunicationMgt.ConvertToCleanOptionValue(Value);
210+
if Enum::"Shpfy Payout Status".Names().Contains(Value) then
211+
exit(Enum::"Shpfy Payout Status".FromInteger(Enum::"Shpfy Payout Status".Ordinals().Get(Enum::"Shpfy Payout Status".Names().IndexOf(Value))))
212+
else
213+
exit(Enum::"Shpfy Payout Status"::Unknown);
214+
end;
215+
216+
local procedure ConvertToPaymentTranscationType(Value: Text): Enum "Shpfy Payment Trans. Type"
217+
begin
218+
Value := CommunicationMgt.ConvertToCleanOptionValue(Value);
219+
if Enum::"Shpfy Payment Trans. Type".Names().Contains(Value) then
220+
exit(Enum::"Shpfy Payment Trans. Type".FromInteger(Enum::"Shpfy Payment Trans. Type".Ordinals().Get(Enum::"Shpfy Payment Trans. Type".Names().IndexOf(Value))))
221+
else
222+
exit(Enum::"Shpfy Payment Trans. Type"::Unknown);
223+
end;
224+
#endregion
225+
226+
#region Disputes
169227
internal procedure ImportDisputes(SinceId: BigInteger)
170228
var
171229
GraphQLType: Enum "Shpfy GraphQL Type";
@@ -251,30 +309,6 @@ codeunit 30385 "Shpfy Payments API"
251309
end;
252310
end;
253311

254-
internal procedure SetShop(ShopifyShop: Record "Shpfy Shop")
255-
begin
256-
Shop := ShopifyShop;
257-
CommunicationMgt.SetShop(Shop);
258-
end;
259-
260-
local procedure ConvertToPayoutStatus(Value: Text): Enum "Shpfy Payout Status"
261-
begin
262-
Value := CommunicationMgt.ConvertToCleanOptionValue(Value);
263-
if Enum::"Shpfy Payout Status".Names().Contains(Value) then
264-
exit(Enum::"Shpfy Payout Status".FromInteger(Enum::"Shpfy Payout Status".Ordinals().Get(Enum::"Shpfy Payout Status".Names().IndexOf(Value))))
265-
else
266-
exit(Enum::"Shpfy Payout Status"::Unknown);
267-
end;
268-
269-
local procedure ConvertToPaymentTranscationType(Value: Text): Enum "Shpfy Payment Trans. Type"
270-
begin
271-
Value := CommunicationMgt.ConvertToCleanOptionValue(Value);
272-
if Enum::"Shpfy Payment Trans. Type".Names().Contains(Value) then
273-
exit(Enum::"Shpfy Payment Trans. Type".FromInteger(Enum::"Shpfy Payment Trans. Type".Ordinals().Get(Enum::"Shpfy Payment Trans. Type".Names().IndexOf(Value))))
274-
else
275-
exit(Enum::"Shpfy Payment Trans. Type"::Unknown);
276-
end;
277-
278312
local procedure ConvertToDisputeStatus(Value: Text): Enum "Shpfy Dispute Status"
279313
begin
280314
Value := CommunicationMgt.ConvertToCleanOptionValue(Value);
@@ -301,4 +335,11 @@ codeunit 30385 "Shpfy Payments API"
301335
else
302336
exit(Enum::"Shpfy Dispute Reason"::Unknown);
303337
end;
338+
#endregion
339+
340+
internal procedure SetShop(ShopifyShop: Record "Shpfy Shop")
341+
begin
342+
Shop := ShopifyShop;
343+
CommunicationMgt.SetShop(Shop);
344+
end;
304345
}

src/Apps/W1/Shopify/App/src/Payments/Reports/ShpfySyncPayments.Report.al

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ report 30105 "Shpfy Sync Payments"
2626
Payments: Codeunit "Shpfy Payments";
2727
begin
2828
Payments.SetShop(Shop);
29-
Payments.SyncPaymentTransactions();
29+
Payments.SyncPayouts();
3030
end;
3131
}
3232
}

src/Apps/W1/Shopify/App/src/PermissionSets/ShpfyObjects.PermissionSet.al

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,8 @@ permissionset 30104 "Shpfy - Objects"
267267
codeunit "Shpfy GQL Payment Terms" = X,
268268
codeunit "Shpfy GQL PaymentTransactions" = X,
269269
codeunit "Shpfy GQL Payouts" = X,
270+
codeunit "Shpfy GQL PayoutsByIds" = X,
271+
codeunit "Shpfy GQL PaymTransByIds" = X,
270272
codeunit "Shpfy GQL ProductById" = X,
271273
codeunit "Shpfy GQL ProductIds" = X,
272274
codeunit "Shpfy GQL ProductImages" = X,

0 commit comments

Comments
 (0)