Skip to content
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

[Shopify] Dispute processing in Shopify (Payments module) #25753

Merged
merged 29 commits into from
Apr 10, 2024
Merged
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9ee36ca
Add Dispute Status and "Dispute Finalized On" DateTime
daniloziva Jan 19, 2024
2a6db49
Fix object name
daniloziva Jan 19, 2024
c6d40b2
Change protection level of Shfy Payment Transaction type
daniloziva Jan 19, 2024
12c822c
Add function DisputesSync() to BackgroundSyncs.Codeunit.al
daniloziva Jan 22, 2024
5159e23
removed from Sync All Action
daniloziva Jan 22, 2024
5cdc133
Tests + CU updates to cater for test implementation
daniloziva Jan 22, 2024
6271f59
Fixes asked for by reviewer
daniloziva Jan 22, 2024
194d366
Updates
daniloziva Jan 26, 2024
d155514
remove variable
daniloziva Jan 26, 2024
106d295
Merge branch 'main' into main
daniloziva Jan 26, 2024
0564ed7
revert versions
daniloziva Jan 26, 2024
e658856
Merge branch 'main' of https://github.com/danilovetatek/ALAppExtensions
daniloziva Jan 26, 2024
17de3d9
Remove Unused var
daniloziva Jan 26, 2024
a66c64f
Add table for Disputes
daniloziva Jan 26, 2024
9717da9
Fix tests
daniloziva Jan 26, 2024
aa14d4c
CodeCopFix
daniloziva Jan 26, 2024
9e9fd0e
fix error
daniloziva Jan 26, 2024
7a2b221
Remove stray code
daniloziva Jan 28, 2024
87cc722
Merge branch 'microsoft:main' into main
daniloziva Feb 1, 2024
a5f0999
Updated enums
daniloziva Feb 5, 2024
6887705
Merge branch 'main' of https://github.com/danilovetatek/ALAppExtensions
daniloziva Feb 5, 2024
476fba8
Merge branch 'main' into main
JesperSchulz Apr 8, 2024
087928e
Apply suggestions from code review
JesperSchulz Apr 8, 2024
a32097d
Update Apps/W1/Shopify/app/src/Payments/Codeunits/ShpfyPayments.Codeu…
JesperSchulz Apr 8, 2024
d461c0c
Update Apps/W1/Shopify/app/src/Payments/Codeunits/ShpfyPayments.Codeu…
JesperSchulz Apr 9, 2024
225d716
Update Apps/W1/Shopify/test/Payments/ShpfyPaymentsTest.Codeunit.al
JesperSchulz Apr 9, 2024
c6770b1
Update test as checked in in NAV repo.
JesperSchulz Apr 10, 2024
39c5d7a
Update Apps/W1/Shopify/app/src/Payments/Codeunits/ShpfyPayments.Codeu…
JesperSchulz Apr 10, 2024
19aaf1d
Apply suggestions from internal check-in.
JesperSchulz Apr 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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>
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
@@ -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;
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
@@ -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;
}
}
}

106 changes: 106 additions & 0 deletions Apps/W1/Shopify/app/src/Payments/Codeunits/ShpfyPayments.Codeunit.al
Original file line number Diff line number Diff line change
@@ -203,4 +203,110 @@ 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;
DisputeStatus: Enum "Shpfy Dispute Status";
Id: BigInteger;
begin
Id := JsonHelper.GetValueAsBigInteger(DisputeToken, 'id');
DisputeStatus := ConvertToDisputeStatus(JsonHelper.GetValueAsText(DisputeToken, 'status'));

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, '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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
namespace Microsoft.Integration.Shopify;

enum 30153 "Shpfy Dispute Reason"
{
/// <summary>
/// Enum Shpfy Dispute Reason (ID 30152).
/// </summary>
///
Caption = 'Shopify Dispute Reason';
Extensible = true;


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';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
namespace Microsoft.Integration.Shopify;
enum 30154 "Shpfy Dispute Status"
{
/// <summary>
/// Enum Shpfy Dispute Status (ID 30150).
/// </summary>

Caption = 'Shopify Dispute Status';

value(0; Unknown)
{
Caption = ' ';
}
/// <summary>
/// The dispute has been open and needs an evidence submission.
/// </summary>
value(1; "Needs Response")
{
Caption = 'Needs Response';
}
/// <summary>
/// The evidence has been submitted and is being reviewed by the cardholder's bank.
/// </summary>
value(2; "Under Review")
{
Caption = 'Under Review';
}
/// <summary>
/// The merchant refunded the inquiry amount.
/// </summary>
value(3; "Charge Refunded")
{
Caption = 'Charge Refunded';
}
/// <summary>
/// The merchant has accepted the dispute as being valid.
/// </summary>
value(4; "Accepted")
{
Caption = 'Accepted';
}
/// <summary>
/// The cardholder's bank reached a final decision in the merchant's favor.
/// </summary>
value(5; "Won")
{
Caption = 'Won';
}
/// <summary>
/// The cardholder's bank reached a final decision in the buyer's favor.
/// </summary>
value(6; "Lost")
{
Caption = 'Lost';
}
}
23 changes: 23 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,23 @@
namespace Microsoft.Integration.Shopify;

enum 30155 "Shpfy Dispute Type"
{
/// <summary>
/// Enum Shpfy Dispute Type (ID 30151).
/// </summary>
///
Caption = 'Shopify Dispute Type';

value(0; Unknown)
{
Caption = ' ';
}
value(1; Inquiry)
{
Caption = 'Inquiry';
}
value(2; Chargeback)
{
Caption = 'Chargeback';
}
}
74 changes: 74 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,74 @@
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.';
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -102,6 +102,21 @@ page 30124 "Shpfy Payment Transactions"
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."Source Order Id");
Page.Run(Page::"Shpfy Disputes", Dispute);
end;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
report 30120 "Shpfy Sync Disputes"
{
ApplicationArea = All;
Caption = 'Shopify Sync Disputes';
ProcessingOnly = true;
UsageCategory = Tasks;

dataset
{
dataitem(Shop; "Shpfy Shop")
{
RequestFilterFields = Code;
trigger OnAfterGetRecord()
var
Sync: Codeunit "Shpfy Payments";
begin
Sync.SetShop(Shop);
Sync.UpdateUnfinishedDisputes();
Sync.ImportNewDisputes();
end;
}
}
}
78 changes: 78 additions & 0 deletions Apps/W1/Shopify/app/src/Payments/Tables/ShpfyDispute.Table.al
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
namespace Microsoft.Integration.Shopify;

/// <summary>
/// Table Shopify Payout (ID 30125).
/// </summary>
table 30155 "Shpfy Dispute"
{
Caption = 'Shopify Dispute';
DataClassification = CustomerContent;

fields
{
field(1; Id; BigInteger)
{
Caption = 'Id';
DataClassification = SystemMetadata;
}
field(2; "Source Order Id"; BigInteger)
{
BlankZero = true;
Caption = 'Source Order Id';
DataClassification = SystemMetadata;
TableRelation = "Shpfy Order Header";
}
field(3; Type; Enum "Shpfy Dispute Type")
{
Caption = 'Type';
DataClassification = CustomerContent;
}
field(4; Currency; Code[10])
{
Caption = 'Currency';
DataClassification = CustomerContent;
}
field(5; Amount; Decimal)
{
Caption = 'Amount';
DataClassification = CustomerContent;
}
field(6; "Reason"; enum "Shpfy Dispute Reason")
{
Caption = 'Shpfy Dispute Reason';
DataClassification = CustomerContent;
}
field(7; "Network Reason Code"; Text[100])
{
Caption = 'Network Reason Code';
DataClassification = CustomerContent;
}
field(8; "Status"; Enum "Shpfy Dispute Status")
{
Caption = 'Status';
DataClassification = CustomerContent;
}
field(9; "Evidence Due By"; DateTime)
{
Caption = 'Evidence Due By';
DataClassification = CustomerContent;
}
field(10; "Evidence Sent On"; DateTime)
{
Caption = 'Evidence Sent On';
DataClassification = CustomerContent;
}
field(11; "Finalized On"; DateTime)
{
Caption = 'Finalized On';
DataClassification = CustomerContent;
}
}
keys
{
key(PK; Id)
{
Clustered = true;
}
}
}
47 changes: 47 additions & 0 deletions Apps/W1/Shopify/test/Payments/ShpfyPaymentsTest.Codeunit.al
Original file line number Diff line number Diff line change
@@ -53,4 +53,51 @@ codeunit 139566 "Shpfy Payments Test"
JPayment.Add('processed_at', Format(CurrentDateTime - 1, 0, 9));
exit(JPayment.AsToken());
end;

[Test]
procedure UnitTestImportDispute()
var
Dispute: Record "Shpfy Dispute";
Payments: Codeunit "Shpfy Payments";
DisputeToken: JsonToken;
DisputeStatus: Enum "Shpfy Dispute Status";
FinalizedOn: DateTime;
Id: BigInteger;
begin
// [SCENARIO] Extract the data out json token that contains a Dispute info into the "Shpfy Dispute" record.
// [GIVEN] A random Generated Dispute
Id := Any.IntegerInRange(10000, 99999);
DisputeToken := GetRandomDisputeAsJsonToken(Id, DisputeStatus, FinalizedOn);

// [WHEN] Invoke the function ImportDisputeData(JToken)
Payments.ImportDisputeData(DisputeToken);

// // [THEN] A dispute record is created and the dispute status and finalized on should match the generated one
Dispute.Get(Id);
LibraryAssert.AreEqual(DisputeStatus, Dispute.Status, 'Dispute status should match the generated one');
LibraryAssert.AreEqual(FinalizedOn, Dispute."Finalized On", 'Dispute finalized on should match the generated one');
end;

local procedure GetRandomDisputeAsJsonToken(Id: BigInteger; var DisputeStatus: Enum "Shpfy Dispute Status"; var FinalizedOn: DateTime): JsonToken
var
DisputeObject: JsonObject;
begin
DisputeStatus := Enum::"Shpfy Dispute Status".FromInteger(Any.IntegerInRange(0, 6));
FinalizedOn := CurrentDateTime - 1;

DisputeObject.Add('id', Id);
DisputeObject.Add('order_id', Any.IntegerInRange(10000, 99999));
DisputeObject.Add('type', 'chargeback');
DisputeObject.Add('amount', Any.DecimalInRange(100, 2));
DisputeObject.Add('currency', Any.IntegerInRange(10000, 99999));
DisputeObject.Add('reason', 'fraudulent');
DisputeObject.Add('network_reason_code', Any.IntegerInRange(10000, 99999));
DisputeObject.Add('status', Format(DisputeStatus));
DisputeObject.Add('evidence_due_by', Format(CurrentDateTime - 1, 0, 9));
DisputeObject.Add('evidence_sent_on', Format(CurrentDateTime - 1, 0, 9));
DisputeObject.Add('finalized_on', Format(FinalizedOn, 0, 9));
DisputeObject.Add('initiated_at', Format(CurrentDateTime - 1, 0, 9));

exit(DisputeObject.AsToken());
end;
}