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