From eaeff6cb28a30d50d33e887594064b6c8a700754 Mon Sep 17 00:00:00 2001 From: daniloziva <123086474+daniloziva@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:41:11 +0200 Subject: [PATCH] [Shopify] Dispute processing in Shopify (Payments module) (#25753) 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 <123086474+danilovetatek@users.noreply.github.com> Co-authored-by: Jesper Schulz-Wedde --- .../ShpfyBackgroundSyncs.Codeunit.al | 31 ++++++ .../app/src/Base/Pages/ShpfyShopCard.Page.al | 17 +++ .../Order handling/Pages/ShpfyOrder.Page.al | 15 +++ .../Codeunits/ShpfyPayments.Codeunit.al | 104 ++++++++++++++++++ .../Payments/Enums/ShpfyDisputeReason.Enum.al | 64 +++++++++++ .../Payments/Enums/ShpfyDisputeStatus.Enum.al | 37 +++++++ .../Payments/Enums/ShpfyDisputeType.Enum.al | 20 ++++ .../src/Payments/Pages/ShpfyDisputes.Page.al | 76 +++++++++++++ .../Pages/ShpfyPaymentTransactions.Page.al | 15 +++ .../Reports/ShpfySyncDisputes.Report.al | 25 +++++ .../src/Payments/Tables/ShpfyDispute.Table.al | 79 +++++++++++++ .../Payments/ShpfyPaymentsTest.Codeunit.al | 47 ++++++++ 12 files changed, 530 insertions(+) create mode 100644 Apps/W1/Shopify/app/src/Payments/Enums/ShpfyDisputeReason.Enum.al create mode 100644 Apps/W1/Shopify/app/src/Payments/Enums/ShpfyDisputeStatus.Enum.al create mode 100644 Apps/W1/Shopify/app/src/Payments/Enums/ShpfyDisputeType.Enum.al create mode 100644 Apps/W1/Shopify/app/src/Payments/Pages/ShpfyDisputes.Page.al create mode 100644 Apps/W1/Shopify/app/src/Payments/Reports/ShpfySyncDisputes.Report.al create mode 100644 Apps/W1/Shopify/app/src/Payments/Tables/ShpfyDispute.Table.al diff --git a/Apps/W1/Shopify/app/src/Base/Codeunits/ShpfyBackgroundSyncs.Codeunit.al b/Apps/W1/Shopify/app/src/Base/Codeunits/ShpfyBackgroundSyncs.Codeunit.al index de7e4eebdc..5691e2e289 100644 --- a/Apps/W1/Shopify/app/src/Base/Codeunits/ShpfyBackgroundSyncs.Codeunit.al +++ b/Apps/W1/Shopify/app/src/Base/Codeunits/ShpfyBackgroundSyncs.Codeunit.al @@ -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; + + /// + /// Payment Dispute Sync. + /// + /// Parameter of type Record "Shopify Shop". + internal procedure DisputesSync(var Shop: Record "Shpfy Shop") + var + Parameters: text; + PaymentParametersTxt: Label '%1', 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; + /// /// Product Images Sync. /// diff --git a/Apps/W1/Shopify/app/src/Base/Pages/ShpfyShopCard.Page.al b/Apps/W1/Shopify/app/src/Base/Pages/ShpfyShopCard.Page.al index 29b64ce181..f740afdf0d 100644 --- a/Apps/W1/Shopify/app/src/Base/Pages/ShpfyShopCard.Page.al +++ b/Apps/W1/Shopify/app/src/Base/Pages/ShpfyShopCard.Page.al @@ -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; diff --git a/Apps/W1/Shopify/app/src/Order handling/Pages/ShpfyOrder.Page.al b/Apps/W1/Shopify/app/src/Order handling/Pages/ShpfyOrder.Page.al index 53c8e4934e..99fdff807f 100644 --- a/Apps/W1/Shopify/app/src/Order handling/Pages/ShpfyOrder.Page.al +++ b/Apps/W1/Shopify/app/src/Order handling/Pages/ShpfyOrder.Page.al @@ -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; + } } } diff --git a/Apps/W1/Shopify/app/src/Payments/Codeunits/ShpfyPayments.Codeunit.al b/Apps/W1/Shopify/app/src/Payments/Codeunits/ShpfyPayments.Codeunit.al index 86a45c6f7c..79c6802c47 100644 --- a/Apps/W1/Shopify/app/src/Payments/Codeunits/ShpfyPayments.Codeunit.al +++ b/Apps/W1/Shopify/app/src/Payments/Codeunits/ShpfyPayments.Codeunit.al @@ -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; } \ No newline at end of file diff --git a/Apps/W1/Shopify/app/src/Payments/Enums/ShpfyDisputeReason.Enum.al b/Apps/W1/Shopify/app/src/Payments/Enums/ShpfyDisputeReason.Enum.al new file mode 100644 index 0000000000..0f06632d25 --- /dev/null +++ b/Apps/W1/Shopify/app/src/Payments/Enums/ShpfyDisputeReason.Enum.al @@ -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'; + } +} \ No newline at end of file diff --git a/Apps/W1/Shopify/app/src/Payments/Enums/ShpfyDisputeStatus.Enum.al b/Apps/W1/Shopify/app/src/Payments/Enums/ShpfyDisputeStatus.Enum.al new file mode 100644 index 0000000000..62a0a82a42 --- /dev/null +++ b/Apps/W1/Shopify/app/src/Payments/Enums/ShpfyDisputeStatus.Enum.al @@ -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'; + } +} \ No newline at end of file diff --git a/Apps/W1/Shopify/app/src/Payments/Enums/ShpfyDisputeType.Enum.al b/Apps/W1/Shopify/app/src/Payments/Enums/ShpfyDisputeType.Enum.al new file mode 100644 index 0000000000..03964346a4 --- /dev/null +++ b/Apps/W1/Shopify/app/src/Payments/Enums/ShpfyDisputeType.Enum.al @@ -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'; + } +} \ No newline at end of file diff --git a/Apps/W1/Shopify/app/src/Payments/Pages/ShpfyDisputes.Page.al b/Apps/W1/Shopify/app/src/Payments/Pages/ShpfyDisputes.Page.al new file mode 100644 index 0000000000..8e8487174e --- /dev/null +++ b/Apps/W1/Shopify/app/src/Payments/Pages/ShpfyDisputes.Page.al @@ -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.'; + } + } + } + } +} \ No newline at end of file diff --git a/Apps/W1/Shopify/app/src/Payments/Pages/ShpfyPaymentTransactions.Page.al b/Apps/W1/Shopify/app/src/Payments/Pages/ShpfyPaymentTransactions.Page.al index 7d87bdf43a..c3f0870834 100644 --- a/Apps/W1/Shopify/app/src/Payments/Pages/ShpfyPaymentTransactions.Page.al +++ b/Apps/W1/Shopify/app/src/Payments/Pages/ShpfyPaymentTransactions.Page.al @@ -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; + } } } } diff --git a/Apps/W1/Shopify/app/src/Payments/Reports/ShpfySyncDisputes.Report.al b/Apps/W1/Shopify/app/src/Payments/Reports/ShpfySyncDisputes.Report.al new file mode 100644 index 0000000000..0190bdc155 --- /dev/null +++ b/Apps/W1/Shopify/app/src/Payments/Reports/ShpfySyncDisputes.Report.al @@ -0,0 +1,25 @@ +namespace Microsoft.Integration.Shopify; + +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; + } + } +} diff --git a/Apps/W1/Shopify/app/src/Payments/Tables/ShpfyDispute.Table.al b/Apps/W1/Shopify/app/src/Payments/Tables/ShpfyDispute.Table.al new file mode 100644 index 0000000000..9673d0e9d2 --- /dev/null +++ b/Apps/W1/Shopify/app/src/Payments/Tables/ShpfyDispute.Table.al @@ -0,0 +1,79 @@ +namespace Microsoft.Integration.Shopify; + +/// +/// Table Shopify Payout (ID 30125). +/// +table 30155 "Shpfy Dispute" +{ + Access = Internal; + 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; + } + } +} diff --git a/Apps/W1/Shopify/test/Payments/ShpfyPaymentsTest.Codeunit.al b/Apps/W1/Shopify/test/Payments/ShpfyPaymentsTest.Codeunit.al index 049b2e060a..74d20809fd 100644 --- a/Apps/W1/Shopify/test/Payments/ShpfyPaymentsTest.Codeunit.al +++ b/Apps/W1/Shopify/test/Payments/ShpfyPaymentsTest.Codeunit.al @@ -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; } \ No newline at end of file