Skip to content

Commit

Permalink
[Shopify] Dispute processing in Shopify (Payments module) (#25753)
Browse files Browse the repository at this point in the history
When there is a dispute related to a payment transaction, there is no
clear way to see the status of the dispute and the finalized date within
BC.

The committed code enables this. I have added an Enum that represents
the dispute statuses from Shopify, and mapped them accordingly.

UpdateDisputeStatus() function in ShpfyPayments.Codeunit.al gets the
list of disputes from the Shopify API, and updates the data in BC.

Considering that dispute processing is an async task and might happen at
any point in time, I pull the data from Shopify via the report "Shpfy
Sync Disputes", that can be scheduled via Job queue.

On another note, with the updates to the Disputes, values from "Shpfy
Payment Transcation Type" would be very valued for extensibility
purposes, so I removed the Internal access.
Fixes #26262
Fixes
[AB#498566](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/498566)

---------

Co-authored-by: danilovetatek <[email protected]>
Co-authored-by: Jesper Schulz-Wedde <[email protected]>
  • Loading branch information
3 people authored Apr 10, 2024
1 parent 3ba9bae commit eaeff6c
Show file tree
Hide file tree
Showing 12 changed files with 530 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,37 @@ codeunit 30101 "Shpfy Background Syncs"
end;
end;

internal procedure DisputesSync(ShopCode: Code[20])
var
Shop: Record "Shpfy Shop";
begin
if Shop.Get(ShopCode) then begin
Shop.SetRecFilter();
DisputesSync(Shop);
end;
end;

/// <summary>
/// Payment Dispute Sync.
/// </summary>
/// <param name="Shop">Parameter of type Record "Shopify Shop".</param>
internal procedure DisputesSync(var Shop: Record "Shpfy Shop")
var
Parameters: text;
PaymentParametersTxt: Label '<?xml version="1.0" standalone="yes"?><ReportParameters name="Shpfy Sync Disputes" id="30105"><DataItems><DataItem name="Shop">%1</DataItem></DataItems></ReportParameters>', Comment = '%1 = Shop Record View', Locked = true;
begin
Shop.SetRange("Allow Background Syncs", true);
if not Shop.IsEmpty then begin
Parameters := StrSubstNo(PaymentParametersTxt, Shop.GetView());
EnqueueJobEntry(Report::"Shpfy Sync Disputes", Parameters, StrSubstNo(SyncDescriptionTxt, PayoutsSyncTypeTxt, Shop.GetFilter(Code)), true, true);
end;
Shop.SetRange("Allow Background Syncs", false);
if not Shop.IsEmpty then begin
Parameters := StrSubstNo(PaymentParametersTxt, Shop.GetView());
EnqueueJobEntry(Report::"Shpfy Sync Disputes", Parameters, StrSubstNo(SyncDescriptionTxt, PayoutsSyncTypeTxt, Shop.GetFilter(Code)), false, true);
end;
end;

/// <summary>
/// Product Images Sync.
/// </summary>
Expand Down
17 changes: 17 additions & 0 deletions Apps/W1/Shopify/app/src/Base/Pages/ShpfyShopCard.Page.al
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,23 @@ page 30101 "Shpfy Shop Card"
Report.Run(Report::"Shpfy Sync Shipm. to Shopify");
end;
}
action(SyncDisputes)
{
ApplicationArea = All;
Caption = 'Sync Disputes';
Image = ErrorLog;
Promoted = true;
PromotedCategory = Category5;
PromotedOnly = true;
ToolTip = 'Synchronize dispute information with related payment transactions.';

trigger OnAction()
var
BackgroundSyncs: Codeunit "Shpfy Background Syncs";
begin
BackgroundSyncs.DisputesSync(Rec.Code);
end;
}
action(SyncAll)
{
ApplicationArea = All;
Expand Down
15 changes: 15 additions & 0 deletions Apps/W1/Shopify/app/src/Order handling/Pages/ShpfyOrder.Page.al
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,21 @@ page 30113 "Shpfy Order"
Page.Run(Page::"Shpfy Data Capture List", DataCapture);
end;
}
action(Disputes)
{
ApplicationArea = All;
Caption = 'Show Related Disputes';
Image = Entry;
ToolTip = 'View the disputes related to order of the selected transaction.';

trigger OnAction();
var
Dispute: Record "Shpfy Dispute";
begin
Dispute.SetRange("Source Order Id", Rec."Shopify Order Id");
Page.Run(Page::"Shpfy Disputes", Dispute);
end;
}
}
}

Expand Down
104 changes: 104 additions & 0 deletions Apps/W1/Shopify/app/src/Payments/Codeunits/ShpfyPayments.Codeunit.al
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,108 @@ codeunit 30169 "Shpfy Payments"
end;
end;
end;

internal procedure ImportNewDisputes()
var
Dispute: Record "Shpfy Dispute";
SinceId: BigInteger;
JDisputes: JsonArray;
JItem: JsonToken;
JResponse: JsonToken;
Url: Text;
UrlTxt: Label 'shopify_payments/disputes.json?since_id=%1', Locked = true;
begin
if Dispute.FindLast() then
SinceId := Dispute.Id;

Url := CommunicationMgt.CreateWebRequestURL(StrSubstNo(UrlTxt, SinceId));

repeat
JResponse := CommunicationMgt.ExecuteWebRequest(Url, 'GET', JResponse, Url);

if JsonHelper.GetJsonArray(JResponse, JDisputes, 'disputes') then
foreach JItem in JDisputes do
ImportDisputeData(JItem);
until Url = '';
end;

internal procedure UpdateUnfinishedDisputes()
var
Dispute: Record "Shpfy Dispute";
DisputeToken: JsonToken;
JResponse: JsonToken;
Url: Text;
UrlTxt: Label 'shopify_payments/disputes/%1.json', Locked = true;
begin
Dispute.SetFilter("Status", '<>%1&<>%2', Dispute."Status"::Won, Dispute."Status"::Lost);

if Dispute.FindSet() then
repeat
Url := CommunicationMgt.CreateWebRequestURL(StrSubstNo(UrlTxt, Dispute.Id));
JResponse := CommunicationMgt.ExecuteWebRequest(Url, 'GET', JResponse);
DisputeToken := JsonHelper.GetJsonToken(JResponse, 'dispute');
ImportDisputeData(DisputeToken);
until Dispute.Next() = 0;
end;

internal procedure ImportDisputeData(DisputeToken: JsonToken)
var
Dispute: Record "Shpfy Dispute";
RecordRef: RecordRef;
Id: BigInteger;
begin
Id := JsonHelper.GetValueAsBigInteger(DisputeToken, 'id');

Clear(Dispute);
if not Dispute.Get(Id) then begin
RecordRef.Open(Database::"Shpfy Dispute");
RecordRef.Init();
JsonHelper.GetValueIntoField(DisputeToken, 'order_id', RecordRef, Dispute.FieldNo("Source Order Id"));
JsonHelper.GetValueIntoField(DisputeToken, 'currency', RecordRef, Dispute.FieldNo(Currency));
JsonHelper.GetValueIntoField(DisputeToken, 'amount', RecordRef, Dispute.FieldNo(Amount));
JsonHelper.GetValueIntoField(DisputeToken, 'network_reason_code', RecordRef, Dispute.FieldNo("Network Reason Code"));
JsonHelper.GetValueIntoField(DisputeToken, 'evidence_due_by', RecordRef, Dispute.FieldNo("Evidence Due By"));
JsonHelper.GetValueIntoField(DisputeToken, 'evidence_sent_on', RecordRef, Dispute.FieldNo("Evidence Sent On"));
JsonHelper.GetValueIntoField(DisputeToken, 'finalized_on', RecordRef, Dispute.FieldNo("Finalized On"));
RecordRef.SetTable(Dispute);
RecordRef.Close();
Dispute.Id := Id;
Dispute.Status := ConvertToDisputeStatus(JsonHelper.GetValueAsText(DisputeToken, 'status'));
Dispute.Type := ConvertToDisputeType(JsonHelper.GetValueAsText(DisputeToken, 'type'));
Dispute.Reason := ConvertToDisputeReason(JsonHelper.GetValueAsText(DisputeToken, 'reason'));
Dispute.Insert();
end else begin
Dispute.Status := ConvertToDisputeStatus(JsonHelper.GetValueAsText(DisputeToken, 'status'));
Dispute."Evidence Sent On" := JsonHelper.GetValueAsDateTime(DisputeToken, 'evidence_due_by');
Dispute."Finalized On" := JsonHelper.GetValueAsDateTime(DisputeToken, 'finalized_on');
Dispute.Modify();
end;
end;

local procedure ConvertToDisputeStatus(Value: Text): Enum "Shpfy Dispute Status"
begin
Value := CommunicationMgt.ConvertToCleanOptionValue(Value);
if Enum::"Shpfy Dispute Status".Names().Contains(Value) then
exit(Enum::"Shpfy Dispute Status".FromInteger(Enum::"Shpfy Dispute Status".Ordinals().Get(Enum::"Shpfy Dispute Status".Names().IndexOf(Value))))
else
exit(Enum::"Shpfy Dispute Status"::Unknown);
end;

local procedure ConvertToDisputeType(Value: Text): Enum "Shpfy Dispute Type"
begin
Value := CommunicationMgt.ConvertToCleanOptionValue(Value);
if Enum::"Shpfy Dispute Type".Names().Contains(Value) then
exit(Enum::"Shpfy Dispute Type".FromInteger(Enum::"Shpfy Dispute Type".Ordinals().Get(Enum::"Shpfy Dispute Type".Names().IndexOf(Value))))
else
exit(Enum::"Shpfy Dispute Type"::Unknown);
end;

local procedure ConvertToDisputeReason(Value: Text): Enum "Shpfy Dispute Reason"
begin
Value := CommunicationMgt.ConvertToCleanOptionValue(Value);
if Enum::"Shpfy Dispute Status".Names().Contains(Value) then
exit(Enum::"Shpfy Dispute Reason".FromInteger(Enum::"Shpfy Dispute Reason".Ordinals().Get(Enum::"Shpfy Dispute Reason".Names().IndexOf(Value))))
else
exit(Enum::"Shpfy Dispute Reason"::Unknown);
end;
}
64 changes: 64 additions & 0 deletions Apps/W1/Shopify/app/src/Payments/Enums/ShpfyDisputeReason.Enum.al
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
namespace Microsoft.Integration.Shopify;

enum 30153 "Shpfy Dispute Reason"
{
Caption = 'Shopify Dispute Reason';
Extensible = false;

value(0; Unknown)
{
Caption = 'Unknown';
}
value(1; "Bank Not Process")
{
Caption = 'Bank Not Process';
}
value(2; "Credit Not Processed")
{
Caption = 'Credit Not Processed';
}
value(3; "Customer Initiated")
{
Caption = 'Customer Initiated';
}
value(4; "Debit Not Authorized")
{
Caption = 'Debit Not Authorized';
}
value(5; Duplicate)
{
Caption = 'Duplicate';
}
value(6; Fraudulent)
{
Caption = 'Fraudulent';
}
value(7; General)
{
Caption = 'General';
}
value(8; "Incorrect Account Details")
{
Caption = 'Incorrect Account Details';
}
value(9; "Insufficient Funds")
{
Caption = 'Insufficient Funds';
}
value(10; "Product Not Received")
{
Caption = 'Product Not Received';
}
value(11; "Product Unacceptable")
{
Caption = 'Product Unacceptable';
}
value(12; "Subscription Canceled")
{
Caption = 'Subscription Canceled';
}
value(13; Unrecognized)
{
Caption = 'Unrecognized';
}
}
37 changes: 37 additions & 0 deletions Apps/W1/Shopify/app/src/Payments/Enums/ShpfyDisputeStatus.Enum.al
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace Microsoft.Integration.Shopify;

enum 30154 "Shpfy Dispute Status"
{

Caption = 'Shopify Dispute Status';
Extensible = false;

value(0; Unknown)
{
Caption = ' ';
}
value(1; "Needs Response")
{
Caption = 'Needs Response';
}
value(2; "Under Review")
{
Caption = 'Under Review';
}
value(3; "Charge Refunded")
{
Caption = 'Charge Refunded';
}
value(4; "Accepted")
{
Caption = 'Accepted';
}
value(5; "Won")
{
Caption = 'Won';
}
value(6; "Lost")
{
Caption = 'Lost';
}
}
20 changes: 20 additions & 0 deletions Apps/W1/Shopify/app/src/Payments/Enums/ShpfyDisputeType.Enum.al
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Microsoft.Integration.Shopify;

enum 30155 "Shpfy Dispute Type"
{
Caption = 'Shopify Dispute Type';
Extensible = false;

value(0; Unknown)
{
Caption = ' ';
}
value(1; Inquiry)
{
Caption = 'Inquiry';
}
value(2; Chargeback)
{
Caption = 'Chargeback';
}
}
76 changes: 76 additions & 0 deletions Apps/W1/Shopify/app/src/Payments/Pages/ShpfyDisputes.Page.al
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
namespace Microsoft.Integration.Shopify;

page 30161 "Shpfy Disputes"
{
Editable = false;
PageType = List;
UsageCategory = None;
SourceTable = "Shpfy Dispute";
Caption = 'Disputes';

layout
{
area(Content)
{
repeater(control01)
{

field(Id; Rec.Id)
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Id field.';
}
field("Source Order Id"; Rec."Source Order Id")
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Source Order Id field.';
}
field("Type"; Rec."Type")
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Type field.';
}
field(Currency; Rec.Currency)
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Currency field.';
}
field(Amount; Rec.Amount)
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Amount field.';
}
field(Reason; Rec.Reason)
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Shpfy Dispute Reason field.';
}
field("Network Reason Code"; Rec."Network Reason Code")
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Network Reason Code field.';
}
field(Status; Rec.Status)
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Status field.';
}
field("Evidence Due By"; Rec."Evidence Due By")
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Evidence Due By field.';
}
field("Evidence Sent On"; Rec."Evidence Sent On")
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Evidence Sent On field.';
}
field("Finalized On"; Rec."Finalized On")
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Finalized On field.';
}
}
}
}
}
Loading

0 comments on commit eaeff6c

Please sign in to comment.