diff --git a/src/Apps/W1/Shopify/App/src/Document Links/Codeunits/ShpfyDocumentLinkMgt.Codeunit.al b/src/Apps/W1/Shopify/App/src/Document Links/Codeunits/ShpfyDocumentLinkMgt.Codeunit.al
index 2c2830ca090..b95417a2796 100644
--- a/src/Apps/W1/Shopify/App/src/Document Links/Codeunits/ShpfyDocumentLinkMgt.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/Document Links/Codeunits/ShpfyDocumentLinkMgt.Codeunit.al
@@ -5,9 +5,11 @@
namespace Microsoft.Integration.Shopify;
+using Microsoft.Finance.GeneralLedger.Journal;
using Microsoft.Sales.Document;
using Microsoft.Sales.History;
using Microsoft.Sales.Posting;
+using Microsoft.Warehouse.Activity;
codeunit 30262 "Shpfy Document Link Mgt."
{
@@ -63,11 +65,9 @@ codeunit 30262 "Shpfy Document Link Mgt."
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnAfterPostSalesDoc', '', true, false)]
- local procedure OnAfterSalesPosting(var SalesHeader: Record "Sales Header"; PreviewMode: Boolean; SalesShptHdrNo: Code[20]; SalesInvHdrNo: Code[20]; RetRcpHdrNo: Code[20]; SalesCrMemoHdrNo: Code[20])
+ local procedure OnAfterSalesPosting(var SalesHeader: Record "Sales Header"; PreviewMode: Boolean; CommitIsSuppressed: Boolean; InvtPickPutaway: Boolean; SalesShptHdrNo: Code[20]; SalesInvHdrNo: Code[20]; RetRcpHdrNo: Code[20]; SalesCrMemoHdrNo: Code[20])
var
- SalesShipmentHeader: Record "Sales Shipment Header";
- SalesInvoiceLine: Record "Sales Invoice Line";
- SalesShipments: List of [Code[20]];
+ ShpfyAutoPostTransactions: Codeunit "Shpfy Auto Post Transactions";
begin
if SalesHeader.IsTemporary() then
exit;
@@ -75,6 +75,54 @@ codeunit 30262 "Shpfy Document Link Mgt."
if PreviewMode then
exit;
+ CreateDocLinksToBCDocs(SalesHeader, SalesShptHdrNo, SalesInvHdrNo, RetRcpHdrNo, SalesCrMemoHdrNo);
+
+ if CommitIsSuppressed or InvtPickPutaway then
+ exit;
+
+ // CreateDocLinksToBCDocs can open a write transaction after Sales-Post's final commit.
+ // Flush those links before invoking the isolated Codeunit.Run posting operations.
+ Commit();
+ ShpfyAutoPostTransactions.AutoPostTransactions(SalesInvHdrNo, SalesCrMemoHdrNo, HasJournalPermissions());
+ end;
+
+ [EventSubscriber(ObjectType::Codeunit, Codeunit::"Whse.-Activity-Post", 'OnAfterPostWhseActivityCompleted', '', false, false)]
+ local procedure OnAfterPostWhseActivityCompleted(WhseActivHeader: Record "Warehouse Activity Header"; var SalesHeader: Record "Sales Header"; SuppressCommit: Boolean; IsPreview: Boolean)
+ var
+ ShpfyAutoPostTransactions: Codeunit "Shpfy Auto Post Transactions";
+ begin
+ if not (WhseActivHeader.Type in [WhseActivHeader.Type::"Invt. Pick", WhseActivHeader.Type::"Invt. Put-away"]) then
+ exit;
+ if SuppressCommit or IsPreview or (SalesHeader."Last Posting No." = '') then
+ exit;
+
+ Commit();
+ case SalesHeader."Document Type" of
+ SalesHeader."Document Type"::Order,
+ SalesHeader."Document Type"::Invoice:
+ ShpfyAutoPostTransactions.AutoPostTransactions(SalesHeader."Last Posting No.", '', HasJournalPermissions());
+ SalesHeader."Document Type"::"Return Order",
+ SalesHeader."Document Type"::"Credit Memo":
+ ShpfyAutoPostTransactions.AutoPostTransactions('', SalesHeader."Last Posting No.", HasJournalPermissions());
+ end;
+ end;
+
+ local procedure HasJournalPermissions(): Boolean
+ var
+ GenJournalBatch: Record "Gen. Journal Batch";
+ GenJournalLine: Record "Gen. Journal Line";
+ begin
+ exit(
+ GenJournalBatch.ReadPermission() and GenJournalBatch.WritePermission() and
+ GenJournalLine.ReadPermission() and GenJournalLine.WritePermission());
+ end;
+
+ local procedure CreateDocLinksToBCDocs(var SalesHeader: Record "Sales Header"; SalesShptHdrNo: Code[20]; SalesInvHdrNo: Code[20]; RetRcpHdrNo: Code[20]; SalesCrMemoHdrNo: Code[20])
+ var
+ SalesShipmentHeader: Record "Sales Shipment Header";
+ SalesInvoiceLine: Record "Sales Invoice Line";
+ SalesShipments: List of [Code[20]];
+ begin
DocLinkToBCDoc.SetRange("Document Type", ShpfyBCDocumentTypeConvert.Convert(SalesHeader."Document Type"));
DocLinkToBCDoc.SetRange("Document No.", SalesHeader."No.");
DocLinkToBCDoc.SetCurrentKey("Document Type", "Document No.");
diff --git a/src/Apps/W1/Shopify/App/src/PermissionSets/ShpfyObjects.PermissionSet.al b/src/Apps/W1/Shopify/App/src/PermissionSets/ShpfyObjects.PermissionSet.al
index eb2de3d3f05..52dd3220b70 100644
--- a/src/Apps/W1/Shopify/App/src/PermissionSets/ShpfyObjects.PermissionSet.al
+++ b/src/Apps/W1/Shopify/App/src/PermissionSets/ShpfyObjects.PermissionSet.al
@@ -103,6 +103,10 @@ permissionset 30104 "Shpfy - Objects"
report "Shpfy Translator" = X,
codeunit "Company Details Checklist Item" = X,
codeunit "Shpfy Authentication Mgt." = X,
+ codeunit "Shpfy Auto Gen. Jnl.-Post" = X,
+ codeunit "Shpfy Auto Post Eligibility" = X,
+ codeunit "Shpfy Auto Post Finalize" = X,
+ codeunit "Shpfy Auto Post Transactions" = X,
codeunit "Shpfy Background Syncs" = X,
codeunit "Shpfy Balance Today" = X,
codeunit "Shpfy Base64" = X,
@@ -301,6 +305,7 @@ permissionset 30104 "Shpfy - Objects"
page "Shpfy Customers" = X,
page "Shpfy Data Capture List" = X,
page "Shpfy Disputes" = X,
+ page "Shpfy Filter Transactions" = X,
page "Shpfy Fulfillment Order Card" = X,
page "Shpfy Fulfillment Order Lines" = X,
page "Shpfy Fulfillment Orders" = X,
diff --git a/src/Apps/W1/Shopify/App/src/Transactions/Codeunits/ShpfyAutoGenJnlPost.Codeunit.al b/src/Apps/W1/Shopify/App/src/Transactions/Codeunits/ShpfyAutoGenJnlPost.Codeunit.al
new file mode 100644
index 00000000000..f4941e43516
--- /dev/null
+++ b/src/Apps/W1/Shopify/App/src/Transactions/Codeunits/ShpfyAutoGenJnlPost.Codeunit.al
@@ -0,0 +1,89 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Integration.Shopify;
+
+using Microsoft.Finance.GeneralLedger.Journal;
+using Microsoft.Finance.GeneralLedger.Posting;
+
+///
+/// Codeunit Shpfy Auto Gen. Jnl.-Post (ID 30422).
+/// Creates a dedicated, single-use journal batch (cloned from the configured one) and builds the general
+/// journal line(s) for a single Shopify order/refund payment transaction into it. It is invoked through
+/// Codeunit.Run so that any failure while creating the batch or building lines is trapped and rolled back
+/// without leaving a batch or line behind. While bound, it also pre-confirms the "posting after working
+/// date" prompt so the automatic posting stays non-interactive.
+///
+codeunit 30422 "Shpfy Auto Gen. Jnl.-Post"
+{
+ Access = Internal;
+ EventSubscriberInstance = Manual;
+ TableNo = "Shpfy Order Transaction";
+
+ trigger OnRun()
+ begin
+ CreateBatchAndBuildLines(Rec);
+ end;
+
+ var
+ PaymentMethodMapping: Record "Shpfy Payment Method Mapping";
+ PostingDate: Date;
+ IsolatedTemplateName: Code[10];
+ IsolatedBatchName: Code[10];
+
+ internal procedure SetParameters(NewPaymentMethodMapping: Record "Shpfy Payment Method Mapping"; NewPostingDate: Date)
+ begin
+ PaymentMethodMapping := NewPaymentMethodMapping;
+ PostingDate := NewPostingDate;
+ Clear(IsolatedTemplateName);
+ Clear(IsolatedBatchName);
+ end;
+
+ internal procedure GetIsolatedBatch(var NewTemplateName: Code[10]; var NewBatchName: Code[10])
+ begin
+ NewTemplateName := IsolatedTemplateName;
+ NewBatchName := IsolatedBatchName;
+ end;
+
+ local procedure CreateBatchAndBuildLines(var OrderTransaction: Record "Shpfy Order Transaction")
+ var
+ SuggestPayments: Report "Shpfy Suggest Payments";
+ begin
+ CreateIsolatedBatch();
+ SuggestPayments.SetJournalParameters(IsolatedTemplateName, IsolatedBatchName, PostingDate);
+ SuggestPayments.GetOrderTransactions(OrderTransaction);
+ SuggestPayments.CreateGeneralJournalLines();
+ end;
+
+ local procedure CreateIsolatedBatch()
+ var
+ ConfiguredBatch: Record "Gen. Journal Batch";
+ IsolatedBatch: Record "Gen. Journal Batch";
+ begin
+ ConfiguredBatch.Get(PaymentMethodMapping."Auto-Post Jnl. Template", PaymentMethodMapping."Auto-Post Jnl. Batch");
+ IsolatedBatch := ConfiguredBatch;
+ IsolatedBatch.Name := GetUniqueBatchName(ConfiguredBatch."Journal Template Name");
+ IsolatedBatch.Insert(true);
+ IsolatedTemplateName := IsolatedBatch."Journal Template Name";
+ IsolatedBatchName := IsolatedBatch.Name;
+ end;
+
+ local procedure GetUniqueBatchName(TemplateName: Code[10]): Code[10]
+ var
+ ExistingBatch: Record "Gen. Journal Batch";
+ CandidateName: Code[10];
+ begin
+ repeat
+ CandidateName := CopyStr('SHPFY' + CopyStr(DelChr(Format(CreateGuid()), '=', '{}-'), 1, 5), 1, 10);
+ until not ExistingBatch.Get(TemplateName, CandidateName);
+ exit(CandidateName);
+ end;
+
+ [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Batch", 'OnBeforeCheckLine', '', false, false)]
+ local procedure PreconfirmWorkingDateOnBeforeCheckLine(var PostingAfterWorkingDateConfirmed: Boolean)
+ begin
+ PostingAfterWorkingDateConfirmed := true;
+ end;
+}
diff --git a/src/Apps/W1/Shopify/App/src/Transactions/Codeunits/ShpfyAutoPostEligibility.Codeunit.al b/src/Apps/W1/Shopify/App/src/Transactions/Codeunits/ShpfyAutoPostEligibility.Codeunit.al
new file mode 100644
index 00000000000..5420bbc3cf4
--- /dev/null
+++ b/src/Apps/W1/Shopify/App/src/Transactions/Codeunits/ShpfyAutoPostEligibility.Codeunit.al
@@ -0,0 +1,85 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Integration.Shopify;
+
+using Microsoft.Sales.Document;
+using Microsoft.Sales.History;
+
+///
+/// Codeunit Shpfy Auto Post Eligibility (ID 30424).
+/// Keeps automatic-posting and transaction-list filtering predicates aligned.
+///
+codeunit 30424 "Shpfy Auto Post Eligibility"
+{
+ Access = Internal;
+
+ internal procedure IsReadyToPost(OrderTransaction: Record "Shpfy Order Transaction"; var PaymentMethodMapping: Record "Shpfy Payment Method Mapping"): Boolean
+ begin
+ exit(
+ GetPaymentMethodMapping(OrderTransaction, PaymentMethodMapping) and
+ IsMappingConfigured(PaymentMethodMapping) and
+ IsTransactionPostable(OrderTransaction));
+ end;
+
+ internal procedure GetPaymentMethodMapping(OrderTransaction: Record "Shpfy Order Transaction"; var PaymentMethodMapping: Record "Shpfy Payment Method Mapping"): Boolean
+ begin
+ exit(PaymentMethodMapping.Get(OrderTransaction.Shop, OrderTransaction.Gateway, OrderTransaction."Credit Card Company"));
+ end;
+
+ internal procedure IsMappingConfigured(PaymentMethodMapping: Record "Shpfy Payment Method Mapping"): Boolean
+ begin
+ exit(
+ PaymentMethodMapping."Post Automatically" and
+ (PaymentMethodMapping."Auto-Post Jnl. Template" <> '') and
+ (PaymentMethodMapping."Auto-Post Jnl. Batch" <> ''));
+ end;
+
+ internal procedure IsTransactionPostable(OrderTransaction: Record "Shpfy Order Transaction"): Boolean
+ begin
+ if OrderTransaction.Status <> OrderTransaction.Status::Success then
+ exit(false);
+ if not (OrderTransaction.Type in [OrderTransaction.Type::Capture, OrderTransaction.Type::Sale, OrderTransaction.Type::Refund]) then
+ exit(false);
+
+ OrderTransaction.CalcFields(Used);
+ if OrderTransaction.Used then
+ exit(false);
+ if OpenSalesDocumentExists(OrderTransaction) then
+ exit(false);
+ exit(PostedDocumentExists(OrderTransaction));
+ end;
+
+ local procedure OpenSalesDocumentExists(OrderTransaction: Record "Shpfy Order Transaction"): Boolean
+ var
+ SalesHeader: Record "Sales Header";
+ begin
+ if OrderTransaction.Type = OrderTransaction.Type::Refund then begin
+ if OrderTransaction."Refund Id" = 0 then
+ exit(true);
+ SalesHeader.SetRange("Shpfy Refund Id", OrderTransaction."Refund Id");
+ exit(not SalesHeader.IsEmpty());
+ end;
+
+ if OrderTransaction."Shopify Order Id" = 0 then
+ exit(true);
+ SalesHeader.SetRange("Shpfy Order Id", OrderTransaction."Shopify Order Id");
+ exit(not SalesHeader.IsEmpty());
+ end;
+
+ local procedure PostedDocumentExists(OrderTransaction: Record "Shpfy Order Transaction"): Boolean
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ begin
+ if OrderTransaction.Type = OrderTransaction.Type::Refund then begin
+ SalesCrMemoHeader.SetRange("Shpfy Refund Id", OrderTransaction."Refund Id");
+ exit(not SalesCrMemoHeader.IsEmpty());
+ end;
+
+ SalesInvoiceHeader.SetRange("Shpfy Order Id", OrderTransaction."Shopify Order Id");
+ exit(not SalesInvoiceHeader.IsEmpty());
+ end;
+}
diff --git a/src/Apps/W1/Shopify/App/src/Transactions/Codeunits/ShpfyAutoPostFinalize.Codeunit.al b/src/Apps/W1/Shopify/App/src/Transactions/Codeunits/ShpfyAutoPostFinalize.Codeunit.al
new file mode 100644
index 00000000000..be3b83e0f13
--- /dev/null
+++ b/src/Apps/W1/Shopify/App/src/Transactions/Codeunits/ShpfyAutoPostFinalize.Codeunit.al
@@ -0,0 +1,67 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Integration.Shopify;
+
+using Microsoft.Finance.GeneralLedger.Journal;
+
+///
+/// Codeunit Shpfy Auto Post Finalize (ID 30423).
+/// Runs cleanup and skipped-record persistence in isolated, trappable transactions.
+///
+codeunit 30423 "Shpfy Auto Post Finalize"
+{
+ Access = Internal;
+ TableNo = "Shpfy Order Transaction";
+ Permissions = tabledata "Gen. Journal Batch" = rimd,
+ tabledata "Gen. Journal Line" = rimd;
+
+ trigger OnRun()
+ begin
+ if CleanupBatch then
+ RemoveIsolatedBatch();
+ if FailureReason <> '' then
+ LogFailure(Rec);
+ end;
+
+ var
+ TemplateName: Code[10];
+ BatchName: Code[10];
+ FailureReason: Text;
+ CleanupBatch: Boolean;
+
+ internal procedure SetCleanupParameters(NewTemplateName: Code[10]; NewBatchName: Code[10])
+ begin
+ TemplateName := NewTemplateName;
+ BatchName := NewBatchName;
+ CleanupBatch := true;
+ Clear(FailureReason);
+ end;
+
+ internal procedure SetFailureParameters(NewFailureReason: Text)
+ begin
+ Clear(TemplateName);
+ Clear(BatchName);
+ CleanupBatch := false;
+ FailureReason := NewFailureReason;
+ end;
+
+ local procedure RemoveIsolatedBatch()
+ var
+ IsolatedBatch: Record "Gen. Journal Batch";
+ begin
+ if IsolatedBatch.Get(TemplateName, BatchName) then
+ IsolatedBatch.Delete(true);
+ end;
+
+ local procedure LogFailure(OrderTransaction: Record "Shpfy Order Transaction")
+ var
+ Shop: Record "Shpfy Shop";
+ SkippedRecord: Codeunit "Shpfy Skipped Record";
+ begin
+ if Shop.Get(OrderTransaction.Shop) then
+ SkippedRecord.LogSkippedRecord(OrderTransaction."Shopify Transaction Id", OrderTransaction.RecordId, CopyStr(FailureReason, 1, 250), Shop);
+ end;
+}
diff --git a/src/Apps/W1/Shopify/App/src/Transactions/Codeunits/ShpfyAutoPostTransactions.Codeunit.al b/src/Apps/W1/Shopify/App/src/Transactions/Codeunits/ShpfyAutoPostTransactions.Codeunit.al
new file mode 100644
index 00000000000..c2caaf5ffae
--- /dev/null
+++ b/src/Apps/W1/Shopify/App/src/Transactions/Codeunits/ShpfyAutoPostTransactions.Codeunit.al
@@ -0,0 +1,217 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Integration.Shopify;
+
+using Microsoft.Finance.GeneralLedger.Journal;
+using Microsoft.Finance.GeneralLedger.Posting;
+using Microsoft.Sales.Document;
+using Microsoft.Sales.History;
+
+///
+/// Codeunit Shpfy Auto Post Transactions (ID 30236).
+/// Automatically posts Shopify order and refund payment transactions as general journal lines when
+/// the related sales invoice or credit memo is posted, provided the transaction's payment method
+/// mapping is configured for automatic posting. Posting is synchronous and best-effort: a failure to
+/// post a payment is logged as a skipped record and never blocks or reverses the document posting.
+/// Each transaction is posted through a dedicated, single-use journal batch so that only the generated
+/// lines are posted and pre-existing lines in the configured batch are never touched.
+///
+codeunit 30236 "Shpfy Auto Post Transactions"
+{
+ Access = Internal;
+
+ internal procedure AutoPostTransactions(SalesInvoiceHeaderNo: Code[20]; SalesCrMemoHeaderNo: Code[20]; HasJournalPermissions: Boolean)
+ begin
+ if SalesInvoiceHeaderNo <> '' then
+ PostOrderTransactions(SalesInvoiceHeaderNo, HasJournalPermissions);
+ if SalesCrMemoHeaderNo <> '' then
+ PostRefundTransactions(SalesCrMemoHeaderNo, HasJournalPermissions);
+ end;
+
+ local procedure PostOrderTransactions(SalesInvoiceHeaderNo: Code[20]; HasJournalPermissions: Boolean)
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ OrderTransaction: Record "Shpfy Order Transaction";
+ begin
+ if not SalesInvoiceHeader.Get(SalesInvoiceHeaderNo) then
+ exit;
+ if SalesInvoiceHeader."Shpfy Order Id" = 0 then
+ exit;
+
+ OrderTransaction.SetRange("Shopify Order Id", SalesInvoiceHeader."Shpfy Order Id");
+ OrderTransaction.SetFilter(Type, '%1|%2', OrderTransaction.Type::Capture, OrderTransaction.Type::Sale);
+ PostTransactions(OrderTransaction, SalesInvoiceHeader."Posting Date", HasJournalPermissions);
+ end;
+
+ local procedure PostRefundTransactions(SalesCrMemoHeaderNo: Code[20]; HasJournalPermissions: Boolean)
+ var
+ SalesCrMemoHeader: Record "Sales Cr.Memo Header";
+ OrderTransaction: Record "Shpfy Order Transaction";
+ begin
+ if not SalesCrMemoHeader.Get(SalesCrMemoHeaderNo) then
+ exit;
+ if SalesCrMemoHeader."Shpfy Refund Id" = 0 then
+ exit;
+
+ OrderTransaction.SetRange("Refund Id", SalesCrMemoHeader."Shpfy Refund Id");
+ OrderTransaction.SetRange(Type, OrderTransaction.Type::Refund);
+ PostTransactions(OrderTransaction, SalesCrMemoHeader."Posting Date", HasJournalPermissions);
+ end;
+
+ local procedure PostTransactions(var OrderTransaction: Record "Shpfy Order Transaction"; PostingDate: Date; HasJournalPermissions: Boolean)
+ var
+ PaymentMethodMapping: Record "Shpfy Payment Method Mapping";
+ AutoGenJnlPost: Codeunit "Shpfy Auto Gen. Jnl.-Post";
+ AutoPostEligibility: Codeunit "Shpfy Auto Post Eligibility";
+ begin
+ OrderTransaction.SetRange(Status, OrderTransaction.Status::Success);
+ OrderTransaction.SetRange(Used, false);
+ if not OrderTransaction.FindSet() then
+ exit;
+
+ repeat
+ if AutoPostEligibility.GetPaymentMethodMapping(OrderTransaction, PaymentMethodMapping) then
+ if PaymentMethodMapping."Post Automatically" then
+ if not AutoPostEligibility.IsMappingConfigured(PaymentMethodMapping) then
+ RecordFailure(OrderTransaction, '', '', ConfigurationStageTok, IncompleteSetupReasonLbl, '')
+ else
+ if AutoPostEligibility.IsTransactionPostable(OrderTransaction) then
+ if HasJournalPermissions then
+ PostTransaction(AutoGenJnlPost, OrderTransaction, PaymentMethodMapping, PostingDate)
+ else
+ RecordFailure(OrderTransaction, '', '', AuthorizationStageTok, InsufficientPermissionsReasonLbl, '');
+ until OrderTransaction.Next() = 0;
+ end;
+
+ local procedure PostTransaction(var AutoGenJnlPost: Codeunit "Shpfy Auto Gen. Jnl.-Post"; OrderTransaction: Record "Shpfy Order Transaction"; PaymentMethodMapping: Record "Shpfy Payment Method Mapping"; PostingDate: Date)
+ var
+ GenJournalLine: Record "Gen. Journal Line";
+ GenJnlPostBatch: Codeunit "Gen. Jnl.-Post Batch";
+ TemplateName: Code[10];
+ BatchName: Code[10];
+ ErrorText: Text;
+ ErrorCallStack: Text;
+ BuildSucceeded: Boolean;
+ PostingSucceeded: Boolean;
+ begin
+ AutoGenJnlPost.SetParameters(PaymentMethodMapping, PostingDate);
+ BindSubscription(AutoGenJnlPost);
+ BuildSucceeded := AutoGenJnlPost.Run(OrderTransaction);
+ if not BuildSucceeded then begin
+ ErrorText := GetLastErrorText();
+ ErrorCallStack := GetLastErrorCallStack();
+ end;
+ UnbindSubscription(AutoGenJnlPost);
+ if not BuildSucceeded then begin
+ RecordFailure(OrderTransaction, '', '', BuildStageTok, ErrorText, ErrorCallStack);
+ exit;
+ end;
+
+ AutoGenJnlPost.GetIsolatedBatch(TemplateName, BatchName);
+ if BatchName = '' then
+ exit;
+
+ GenJournalLine.SetRange("Journal Template Name", TemplateName);
+ GenJournalLine.SetRange("Journal Batch Name", BatchName);
+ if not GenJournalLine.FindSet() then begin
+ CleanupBatch(OrderTransaction, TemplateName, BatchName);
+ exit;
+ end;
+
+ BindSubscription(AutoGenJnlPost);
+ PostingSucceeded := GenJnlPostBatch.Run(GenJournalLine);
+ if not PostingSucceeded then begin
+ ErrorText := GetLastErrorText();
+ ErrorCallStack := GetLastErrorCallStack();
+ end;
+ UnbindSubscription(AutoGenJnlPost);
+ if PostingSucceeded then begin
+ CleanupBatch(OrderTransaction, TemplateName, BatchName);
+ exit;
+ end;
+
+ RecordFailure(OrderTransaction, TemplateName, BatchName, PostingStageTok, ErrorText, ErrorCallStack);
+ end;
+
+ local procedure RecordFailure(OrderTransaction: Record "Shpfy Order Transaction"; TemplateName: Code[10]; BatchName: Code[10]; Stage: Text; ErrorText: Text; ErrorCallStack: Text)
+ begin
+ LogFailureTelemetry(OrderTransaction, TemplateName, BatchName, Stage, ErrorText, ErrorCallStack);
+ if BatchName <> '' then
+ CleanupBatch(OrderTransaction, TemplateName, BatchName);
+ PersistFailure(OrderTransaction, TemplateName, BatchName, ErrorText);
+ end;
+
+ local procedure CleanupBatch(OrderTransaction: Record "Shpfy Order Transaction"; TemplateName: Code[10]; BatchName: Code[10])
+ var
+ AutoPostFinalize: Codeunit "Shpfy Auto Post Finalize";
+ FinalizeErrorText: Text;
+ FinalizeErrorCallStack: Text;
+ begin
+ AutoPostFinalize.SetCleanupParameters(TemplateName, BatchName);
+ if AutoPostFinalize.Run(OrderTransaction) then
+ exit;
+
+ FinalizeErrorText := GetLastErrorText();
+ FinalizeErrorCallStack := GetLastErrorCallStack();
+ LogFinalizationFailureTelemetry(OrderTransaction, TemplateName, BatchName, CleanupStageTok, FinalizeErrorText, FinalizeErrorCallStack);
+ end;
+
+ local procedure PersistFailure(OrderTransaction: Record "Shpfy Order Transaction"; TemplateName: Code[10]; BatchName: Code[10]; ErrorText: Text)
+ var
+ AutoPostFinalize: Codeunit "Shpfy Auto Post Finalize";
+ FinalizeErrorText: Text;
+ FinalizeErrorCallStack: Text;
+ begin
+ AutoPostFinalize.SetFailureParameters(ErrorText);
+ if AutoPostFinalize.Run(OrderTransaction) then
+ exit;
+
+ FinalizeErrorText := GetLastErrorText();
+ FinalizeErrorCallStack := GetLastErrorCallStack();
+ LogFinalizationFailureTelemetry(OrderTransaction, TemplateName, BatchName, FailureLogStageTok, FinalizeErrorText, FinalizeErrorCallStack);
+ end;
+
+ local procedure LogFailureTelemetry(OrderTransaction: Record "Shpfy Order Transaction"; TemplateName: Code[10]; BatchName: Code[10]; Stage: Text; ErrorText: Text; ErrorCallStack: Text)
+ var
+ CustomDimensions: Dictionary of [Text, Text];
+ begin
+ AddTelemetryDimensions(CustomDimensions, OrderTransaction, TemplateName, BatchName, Stage, ErrorText, ErrorCallStack);
+ Session.LogMessage('0000S2A', AutoPostFailedTelemetryMsg, Verbosity::Warning, DataClassification::CustomerContent, TelemetryScope::ExtensionPublisher, CustomDimensions);
+ end;
+
+ local procedure LogFinalizationFailureTelemetry(OrderTransaction: Record "Shpfy Order Transaction"; TemplateName: Code[10]; BatchName: Code[10]; Stage: Text; ErrorText: Text; ErrorCallStack: Text)
+ var
+ CustomDimensions: Dictionary of [Text, Text];
+ begin
+ AddTelemetryDimensions(CustomDimensions, OrderTransaction, TemplateName, BatchName, Stage, ErrorText, ErrorCallStack);
+ Session.LogMessage('0000S2B', AutoPostFinalizationFailedTelemetryMsg, Verbosity::Warning, DataClassification::CustomerContent, TelemetryScope::ExtensionPublisher, CustomDimensions);
+ end;
+
+ local procedure AddTelemetryDimensions(var CustomDimensions: Dictionary of [Text, Text]; OrderTransaction: Record "Shpfy Order Transaction"; TemplateName: Code[10]; BatchName: Code[10]; Stage: Text; ErrorText: Text; ErrorCallStack: Text)
+ begin
+ CustomDimensions.Add('Category', CategoryTok);
+ CustomDimensions.Add('Stage', Stage);
+ CustomDimensions.Add('Shop Code', OrderTransaction.Shop);
+ CustomDimensions.Add('Shopify Transaction Id', Format(OrderTransaction."Shopify Transaction Id"));
+ CustomDimensions.Add('Journal Template Name', TemplateName);
+ CustomDimensions.Add('Journal Batch Name', BatchName);
+ CustomDimensions.Add('Error Text', CopyStr(ErrorText, 1, 250));
+ CustomDimensions.Add('Error Call Stack', CopyStr(ErrorCallStack, 1, 2048));
+ end;
+
+ var
+ AutoPostFailedTelemetryMsg: Label 'Automatic Shopify transaction posting failed.', Locked = true;
+ AutoPostFinalizationFailedTelemetryMsg: Label 'Finalizing a failed automatic Shopify transaction posting attempt failed.', Locked = true;
+ IncompleteSetupReasonLbl: Label 'Automatic posting is enabled, but the journal template or journal batch is not configured.';
+ InsufficientPermissionsReasonLbl: Label 'Automatic posting requires permission to read and write general journal batches and lines.';
+ CategoryTok: Label 'Shopify Integration', Locked = true;
+ AuthorizationStageTok: Label 'Authorization', Locked = true;
+ BuildStageTok: Label 'Build journal lines', Locked = true;
+ CleanupStageTok: Label 'Cleanup journal batch', Locked = true;
+ ConfigurationStageTok: Label 'Configuration', Locked = true;
+ FailureLogStageTok: Label 'Log skipped record', Locked = true;
+ PostingStageTok: Label 'Post journal batch', Locked = true;
+}
diff --git a/src/Apps/W1/Shopify/App/src/Transactions/Pages/ShpfyFilterTransactions.Page.al b/src/Apps/W1/Shopify/App/src/Transactions/Pages/ShpfyFilterTransactions.Page.al
new file mode 100644
index 00000000000..194ebbcca1c
--- /dev/null
+++ b/src/Apps/W1/Shopify/App/src/Transactions/Pages/ShpfyFilterTransactions.Page.al
@@ -0,0 +1,70 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Integration.Shopify;
+
+///
+/// Page Shpfy Filter Transactions (ID 30176).
+/// Request dialog used to filter the Shopify transactions list down to the transactions that are
+/// ready to be posted, optionally narrowed by payment gateway and creation date range.
+///
+page 30176 "Shpfy Filter Transactions"
+{
+ Caption = 'Filter Postable Transactions';
+ PageType = StandardDialog;
+ ApplicationArea = All;
+
+ layout
+ {
+ area(Content)
+ {
+ field(Gateway; Gateway)
+ {
+ Caption = 'Gateway';
+ ToolTip = 'Specifies the transaction gateway to filter transactions by. Leave blank to include all gateways set up for automatic posting.';
+ Editable = false;
+
+ trigger OnAssistEdit()
+ var
+ PaymentMethodMapping: Record "Shpfy Payment Method Mapping";
+ begin
+ PaymentMethodMapping.SetRange("Post Automatically", true);
+ if Page.RunModal(Page::"Shpfy Payment Methods Mapping", PaymentMethodMapping) = Action::LookupOK then begin
+ ShopCode := PaymentMethodMapping."Shop Code";
+ Gateway := PaymentMethodMapping.Gateway;
+ CreditCardCompany := PaymentMethodMapping."Credit Card Company";
+ end;
+ end;
+ }
+ field(StartDate; StartDate)
+ {
+ Caption = 'Start Date';
+ ToolTip = 'Specifies the earliest transaction creation date to include in the filter. Leave blank to include all transactions from the beginning.';
+ }
+ field(EndDate; EndDate)
+ {
+ Caption = 'End Date';
+ ToolTip = 'Specifies the latest transaction creation date to include in the filter. Leave blank to include all transactions.';
+ }
+ }
+ }
+
+ var
+ ShopCode: Code[20];
+ Gateway: Text[30];
+ CreditCardCompany: Text[50];
+ StartDate: Date;
+ EndDate: Date;
+
+ internal procedure GetParameters(var NewShopCode: Code[20]; var NewGateway: Text[30]; var NewCreditCardCompany: Text[50]; var NewStartDate: DateTime; var NewEndDate: DateTime)
+ begin
+ NewShopCode := ShopCode;
+ NewGateway := Gateway;
+ NewCreditCardCompany := CreditCardCompany;
+ NewStartDate := CreateDateTime(StartDate, 0T);
+ if EndDate <> 0D then
+ NewEndDate := CreateDateTime(EndDate, 235959.999T);
+ end;
+}
diff --git a/src/Apps/W1/Shopify/App/src/Transactions/Pages/ShpfyPaymentMethodsMapping.Page.al b/src/Apps/W1/Shopify/App/src/Transactions/Pages/ShpfyPaymentMethodsMapping.Page.al
index e5cab053edf..81d1e808229 100644
--- a/src/Apps/W1/Shopify/App/src/Transactions/Pages/ShpfyPaymentMethodsMapping.Page.al
+++ b/src/Apps/W1/Shopify/App/src/Transactions/Pages/ShpfyPaymentMethodsMapping.Page.al
@@ -37,6 +37,23 @@ page 30132 "Shpfy Payment Methods Mapping"
ApplicationArea = All;
ToolTip = 'Specifies the corresponding payment method in D365BC.';
}
+ field(AutoPostJnlTemplate; Rec."Auto-Post Jnl. Template")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Specifies the general journal template to use for automatically posting payment transactions.';
+ ShowMandatory = Rec."Post Automatically";
+ }
+ field(AutoPostJnlBatch; Rec."Auto-Post Jnl. Batch")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Specifies the general journal batch to use for automatically posting payment transactions.';
+ ShowMandatory = Rec."Post Automatically";
+ }
+ field(PostAutomatically; Rec."Post Automatically")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Specifies whether payment transactions using this gateway should be automatically posted when the related invoice or credit memo is posted. Set the journal template and batch first.';
+ }
}
}
}
diff --git a/src/Apps/W1/Shopify/App/src/Transactions/Pages/ShpfyTransactions.Page.al b/src/Apps/W1/Shopify/App/src/Transactions/Pages/ShpfyTransactions.Page.al
index c76f6158394..8cf3f446d66 100644
--- a/src/Apps/W1/Shopify/App/src/Transactions/Pages/ShpfyTransactions.Page.al
+++ b/src/Apps/W1/Shopify/App/src/Transactions/Pages/ShpfyTransactions.Page.al
@@ -5,6 +5,7 @@
namespace Microsoft.Integration.Shopify;
+using Microsoft.Sales.History;
using Microsoft.Sales.Receivables;
///
@@ -142,6 +143,11 @@ page 30134 "Shpfy Transactions"
ApplicationArea = All;
ToolTip = 'Specifies the Posted Invoice number to which the transaction relates.';
}
+ field("Auto-Post Enabled"; Rec."Auto-Post Enabled")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Specifies whether the transaction is automatically posted when the related invoice or credit memo is posted.';
+ }
}
}
}
@@ -199,12 +205,39 @@ page 30134 "Shpfy Transactions"
SuggestPayments.Run();
end;
}
+ action(ShowPostableTransactions)
+ {
+ ApplicationArea = All;
+ Caption = 'Filter Postable Transactions';
+ Image = FilterLines;
+ ToolTip = 'Show transactions that are ready to be posted: auto-post enabled, successful, not yet posted, and linked to a posted invoice or credit memo. Optionally filter by gateway and date range.';
+
+ trigger OnAction()
+ begin
+ FilterPostableTransactions();
+ end;
+ }
+ action(ClearFilter)
+ {
+ ApplicationArea = All;
+ Caption = 'Clear Filter';
+ Image = ClearFilter;
+ ToolTip = 'Remove the postable transactions filter and show all transactions.';
+
+ trigger OnAction()
+ begin
+ Rec.ClearMarks();
+ Rec.MarkedOnly(false);
+ end;
+ }
}
area(Promoted)
{
group(Category_Process)
{
actionref(SuggestShopifyPayments_Promoted; SuggestShopifyPayments) { }
+ actionref(ShowPostableTransactions_Promoted; ShowPostableTransactions) { }
+ actionref(ClearFilter_Promoted; ClearFilter) { }
}
group(Category_Inspect)
{
@@ -237,4 +270,65 @@ page 30134 "Shpfy Transactions"
PresentmentCurrencyVisible := OrderHeader.IsPresentmentCurrencyOrder();
end;
+
+ local procedure FilterPostableTransactions()
+ var
+ FilterTransactions: Page "Shpfy Filter Transactions";
+ FilterShopCode: Code[20];
+ FilterGateway: Text[30];
+ FilterCreditCardCompany: Text[50];
+ FilterStartDate: DateTime;
+ FilterEndDate: DateTime;
+ begin
+ if FilterTransactions.RunModal() <> Action::OK then
+ exit;
+
+ FilterTransactions.GetParameters(FilterShopCode, FilterGateway, FilterCreditCardCompany, FilterStartDate, FilterEndDate);
+
+ // Apply the same eligibility predicates the posting routine uses.
+ Rec.SetRange(Used, false);
+ Rec.SetRange(Status, Rec.Status::Success);
+ Rec.SetFilter(Type, '%1|%2|%3', Rec.Type::Capture, Rec.Type::Sale, Rec.Type::Refund);
+ if (FilterStartDate <> 0DT) or (FilterEndDate <> 0DT) then
+ Rec.SetRange("Created At", FilterStartDate, GetEndDateFilter(FilterEndDate));
+ if FilterGateway <> '' then begin
+ Rec.SetRange(Shop, FilterShopCode);
+ Rec.SetRange(Gateway, FilterGateway);
+ Rec.SetRange("Credit Card Company", FilterCreditCardCompany);
+ end;
+
+ Rec.ClearMarks();
+ Rec.MarkedOnly(false);
+ MarkPostableTransactions();
+
+ Rec.MarkedOnly(true);
+ Rec.SetRange(Shop);
+ Rec.SetRange(Gateway);
+ Rec.SetRange("Credit Card Company");
+ Rec.SetRange("Created At");
+ Rec.SetRange(Used);
+ Rec.SetRange(Status);
+ Rec.SetRange(Type);
+ end;
+
+ local procedure GetEndDateFilter(FilterEndDate: DateTime): DateTime
+ begin
+ if FilterEndDate <> 0DT then
+ exit(FilterEndDate);
+ exit(CreateDateTime(DMY2Date(31, 12, 9999), 0T));
+ end;
+
+ local procedure MarkPostableTransactions()
+ var
+ PaymentMethodMapping: Record "Shpfy Payment Method Mapping";
+ AutoPostEligibility: Codeunit "Shpfy Auto Post Eligibility";
+ begin
+ Rec.SetLoadFields("Shopify Order Id", Shop, Gateway, "Credit Card Company", Type, Status, "Refund Id");
+ if not Rec.FindSet() then
+ exit;
+ repeat
+ if AutoPostEligibility.IsReadyToPost(Rec, PaymentMethodMapping) then
+ Rec.Mark(true);
+ until Rec.Next() = 0;
+ end;
}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/App/src/Transactions/Reports/ShpfySuggestPayments.Report.al b/src/Apps/W1/Shopify/App/src/Transactions/Reports/ShpfySuggestPayments.Report.al
index 8f95f488356..2f657461f5b 100644
--- a/src/Apps/W1/Shopify/App/src/Transactions/Reports/ShpfySuggestPayments.Report.al
+++ b/src/Apps/W1/Shopify/App/src/Transactions/Reports/ShpfySuggestPayments.Report.al
@@ -210,6 +210,16 @@ report 30118 "Shpfy Suggest Payments"
IgnorePostedTransactions := NewIgnorePostedTransactions;
end;
+ internal procedure SetJournalParameters(NewTemplateName: Code[10]; NewBatchName: Code[10]; NewPostingDate: Date)
+ begin
+ GeneralJournalTemplateName := NewTemplateName;
+ GeneralJournalBatchName := NewBatchName;
+ GenJournalLine."Journal Template Name" := NewTemplateName;
+ GenJournalLine."Journal Batch Name" := NewBatchName;
+ PostingDate := NewPostingDate;
+ ValidatePostingDate();
+ end;
+
local procedure ValidatePostingDate()
var
NoSeries: Codeunit "No. Series";
@@ -252,7 +262,7 @@ report 30118 "Shpfy Suggest Payments"
repeat
if SalesInvoiceHeader.Closed then
continue;
- ApplyCustomerLedgerEntries(SalesInvoiceHeader."No.", "Gen. Journal Document Type"::Invoice, AmountToApply, Applied);
+ ApplyCustomerLedgerEntries(OrderTransaction, SalesInvoiceHeader."No.", "Gen. Journal Document Type"::Invoice, AmountToApply, Applied);
until SalesInvoiceHeader.Next() = 0
else begin
DocLinkToDoc.SetRange("Shopify Document Type", DocLinkToDoc."Shopify Document Type"::"Shopify Shop Order");
@@ -263,12 +273,12 @@ report 30118 "Shpfy Suggest Payments"
SalesInvoiceHeader.Get(DocLinkToDoc."Document No.");
if SalesInvoiceHeader.Closed then
continue;
- ApplyCustomerLedgerEntries(SalesInvoiceHeader."No.", "Gen. Journal Document Type"::Invoice, AmountToApply, Applied);
+ ApplyCustomerLedgerEntries(OrderTransaction, SalesInvoiceHeader."No.", "Gen. Journal Document Type"::Invoice, AmountToApply, Applied);
until DocLinkToDoc.Next() = 0;
end;
if Applied and (AmountToApply > 0) then
- CreateSuggestPaymentGLAccount(AmountToApply, true);
+ CreateSuggestPaymentGLAccount(OrderTransaction, AmountToApply, true);
end;
OrderTransaction.Type::Refund:
begin
@@ -283,18 +293,18 @@ report 30118 "Shpfy Suggest Payments"
repeat
if SalesCreditMemoHeader.Paid then
continue;
- ApplyCustomerLedgerEntries(SalesCreditMemoHeader."No.", "Gen. Journal Document Type"::"Credit Memo", AmountToApply, Applied);
+ ApplyCustomerLedgerEntries(OrderTransaction, SalesCreditMemoHeader."No.", "Gen. Journal Document Type"::"Credit Memo", AmountToApply, Applied);
until SalesCreditMemoHeader.Next() = 0;
until RefundHeader.Next() = 0;
if Applied and (AmountToApply > 0) then
- CreateSuggestPaymentGLAccount(AmountToApply, false);
+ CreateSuggestPaymentGLAccount(OrderTransaction, AmountToApply, false);
end;
end;
end;
end;
- local procedure ApplyCustomerLedgerEntries(DocumentNo: Code[20]; DocumentType: Enum "Gen. Journal Document Type"; var AmountToApply: Decimal; var Applied: Boolean)
+ local procedure ApplyCustomerLedgerEntries(OrderTransaction: Record "Shpfy Order Transaction"; DocumentNo: Code[20]; DocumentType: Enum "Gen. Journal Document Type"; var AmountToApply: Decimal; var Applied: Boolean)
var
CustLedgerEntry: Record "Cust. Ledger Entry";
begin
@@ -307,12 +317,12 @@ report 30118 "Shpfy Suggest Payments"
if CustLedgerEntry.FindSet() then begin
Applied := true;
repeat
- CreateSuggestPaymentDocument(CustLedgerEntry, AmountToApply, DocumentType = "Gen. Journal Document Type"::Invoice);
+ CreateSuggestPaymentDocument(CustLedgerEntry, OrderTransaction, AmountToApply, DocumentType = "Gen. Journal Document Type"::Invoice);
until CustLedgerEntry.Next() = 0;
end;
end;
- local procedure CreateSuggestPaymentDocument(var CustLedgerEntry: Record "Cust. Ledger Entry"; var AmountToApply: Decimal; IsInvoice: Boolean)
+ local procedure CreateSuggestPaymentDocument(var CustLedgerEntry: Record "Cust. Ledger Entry"; OrderTransaction: Record "Shpfy Order Transaction"; var AmountToApply: Decimal; IsInvoice: Boolean)
begin
TempSuggestPayment.Init();
EntryNo += 1;
@@ -346,7 +356,7 @@ report 30118 "Shpfy Suggest Payments"
TempSuggestPayment.Insert();
end;
- local procedure CreateSuggestPaymentGLAccount(AmountToApply: Decimal; IsInvoice: Boolean)
+ local procedure CreateSuggestPaymentGLAccount(OrderTransaction: Record "Shpfy Order Transaction"; AmountToApply: Decimal; IsInvoice: Boolean)
begin
TempSuggestPayment.Init();
EntryNo += 1;
diff --git a/src/Apps/W1/Shopify/App/src/Transactions/Tables/ShpfyOrderTransaction.Table.al b/src/Apps/W1/Shopify/App/src/Transactions/Tables/ShpfyOrderTransaction.Table.al
index 8099a2abca1..6fbabb55fe2 100644
--- a/src/Apps/W1/Shopify/App/src/Transactions/Tables/ShpfyOrderTransaction.Table.al
+++ b/src/Apps/W1/Shopify/App/src/Transactions/Tables/ShpfyOrderTransaction.Table.al
@@ -209,6 +209,17 @@ table 30133 "Shpfy Order Transaction"
DataClassification = SystemMetadata;
Editable = false;
}
+ field(100; "Auto-Post Enabled"; Boolean)
+ {
+ Caption = 'Auto-Post Enabled';
+ FieldClass = FlowField;
+ CalcFormula = exist("Shpfy Payment Method Mapping" where("Shop Code" = field("Shop"),
+ Gateway = field(Gateway),
+ "Credit Card Company" = field("Credit Card Company"),
+ "Post Automatically" = const(true),
+ "Auto-Post Jnl. Template" = filter(<> ''),
+ "Auto-Post Jnl. Batch" = filter(<> '')));
+ }
field(101; "Sales Document No."; code[20])
{
Caption = 'Sales Document No.';
diff --git a/src/Apps/W1/Shopify/App/src/Transactions/Tables/ShpfyPaymentMethodMapping.Table.al b/src/Apps/W1/Shopify/App/src/Transactions/Tables/ShpfyPaymentMethodMapping.Table.al
index e1ee2fb335f..0d0e5afab62 100644
--- a/src/Apps/W1/Shopify/App/src/Transactions/Tables/ShpfyPaymentMethodMapping.Table.al
+++ b/src/Apps/W1/Shopify/App/src/Transactions/Tables/ShpfyPaymentMethodMapping.Table.al
@@ -6,6 +6,7 @@
namespace Microsoft.Integration.Shopify;
using Microsoft.Bank.BankAccount;
+using Microsoft.Finance.GeneralLedger.Journal;
///
/// Table Shpfy Payment Method Mapping (ID 30134).
@@ -15,6 +16,8 @@ table 30134 "Shpfy Payment Method Mapping"
Access = Internal;
Caption = 'Shopify Payment Method';
DataClassification = CustomerContent;
+ DrillDownPageId = "Shpfy Payment Methods Mapping";
+ LookupPageId = "Shpfy Payment Methods Mapping";
fields
{
@@ -30,7 +33,7 @@ table 30134 "Shpfy Payment Method Mapping"
DataClassification = CustomerContent;
TableRelation = "Shpfy Transaction Gateway";
}
- field(3; "Credit Card Company"; Text[30])
+ field(3; "Credit Card Company"; Text[50])
{
Caption = 'Credit Card Company';
DataClassification = CustomerContent;
@@ -59,6 +62,52 @@ table 30134 "Shpfy Payment Method Mapping"
DataClassification = SystemMetadata;
Editable = false;
}
+ field(7; "Post Automatically"; Boolean)
+ {
+ Caption = 'Post Automatically';
+ DataClassification = SystemMetadata;
+
+ trigger OnValidate()
+ begin
+ if "Post Automatically" then begin
+ TestField("Auto-Post Jnl. Template");
+ TestField("Auto-Post Jnl. Batch");
+ end;
+ end;
+ }
+ field(8; "Auto-Post Jnl. Template"; Code[10])
+ {
+ Caption = 'Auto-Post Journal Template';
+ DataClassification = SystemMetadata;
+ TableRelation = "Gen. Journal Template" where(Type = const("Cash Receipts"));
+
+ trigger OnValidate()
+ begin
+ if "Auto-Post Jnl. Template" <> xRec."Auto-Post Jnl. Template" then begin
+ Clear("Auto-Post Jnl. Batch");
+ "Post Automatically" := false;
+ end;
+ end;
+ }
+ field(9; "Auto-Post Jnl. Batch"; Code[10])
+ {
+ Caption = 'Auto-Post Journal Batch';
+ DataClassification = SystemMetadata;
+ TableRelation = "Gen. Journal Batch".Name where("Journal Template Name" = field("Auto-Post Jnl. Template"));
+
+ trigger OnValidate()
+ var
+ GenJournalBatch: Record "Gen. Journal Batch";
+ begin
+ if "Auto-Post Jnl. Batch" = '' then begin
+ "Post Automatically" := false;
+ exit;
+ end;
+ TestField("Auto-Post Jnl. Template");
+ GenJournalBatch.Get("Auto-Post Jnl. Template", "Auto-Post Jnl. Batch");
+ GenJournalBatch.TestField("Bal. Account No.");
+ end;
+ }
}
keys
{
@@ -68,4 +117,21 @@ table 30134 "Shpfy Payment Method Mapping"
}
}
+ trigger OnInsert()
+ begin
+ ValidateAutoPostSetup();
+ end;
+
+ trigger OnModify()
+ begin
+ ValidateAutoPostSetup();
+ end;
+
+ local procedure ValidateAutoPostSetup()
+ begin
+ if not "Post Automatically" then
+ exit;
+ TestField("Auto-Post Jnl. Template");
+ TestField("Auto-Post Jnl. Batch");
+ end;
}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/Test/Payments/ShpfyAutoPostTransTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Payments/ShpfyAutoPostTransTest.Codeunit.al
new file mode 100644
index 00000000000..83c0fb8f4f0
--- /dev/null
+++ b/src/Apps/W1/Shopify/Test/Payments/ShpfyAutoPostTransTest.Codeunit.al
@@ -0,0 +1,997 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Integration.Shopify.Test;
+
+using Microsoft.Finance.GeneralLedger.Account;
+using Microsoft.Finance.GeneralLedger.Journal;
+using Microsoft.Finance.GeneralLedger.Preview;
+using Microsoft.Finance.GeneralLedger.Setup;
+using Microsoft.Foundation.AuditCodes;
+using Microsoft.Foundation.NoSeries;
+using Microsoft.Integration.Shopify;
+using Microsoft.Inventory.Item;
+using Microsoft.Sales.Customer;
+using Microsoft.Sales.Document;
+using Microsoft.Sales.History;
+using Microsoft.Sales.Posting;
+using Microsoft.Sales.Receivables;
+using Microsoft.Utilities;
+using System.Environment.Configuration;
+using System.TestLibraries.Utilities;
+
+///
+/// Codeunit Shpfy Auto Post Trans. Test (ID 139415).
+///
+codeunit 139415 "Shpfy Auto Post Trans. Test"
+{
+ Subtype = Test;
+ TestType = IntegrationTest;
+ TestPermissions = Disabled;
+
+ var
+ Customer: Record Customer;
+ Item: Record Item;
+ Shop: Record "Shpfy Shop";
+ PaymentMethodMapping: Record "Shpfy Payment Method Mapping";
+ LibraryAssert: Codeunit "Library Assert";
+ LibraryERM: Codeunit "Library - ERM";
+ LibraryRandom: Codeunit "Library - Random";
+ LibrarySales: Codeunit "Library - Sales";
+ IsInitialized: Boolean;
+
+ [Test]
+ procedure UnitTestAutoPostJnlBatchValidateWithBalAccountNo()
+ var
+ ShpfyPaymentMethodMapping: Record "Shpfy Payment Method Mapping";
+ GenJournalBatch: Record "Gen. Journal Batch";
+ begin
+ // [SCENARIO] Auto-Post Jnl. Batch field validates successfully when the journal batch has a balancing account number
+
+ // [GIVEN] A Gen. Journal Batch with a balancing account number
+ CreateJournalBatch(GenJournalBatch);
+ ShpfyPaymentMethodMapping."Auto-Post Jnl. Template" := GenJournalBatch."Journal Template Name";
+
+ // [WHEN] Auto-Post Jnl. Batch is validated
+ ShpfyPaymentMethodMapping.Validate("Auto-Post Jnl. Batch", GenJournalBatch.Name);
+
+ // [THEN] Validation passes without error
+ LibraryAssert.AreEqual(GenJournalBatch.Name, ShpfyPaymentMethodMapping."Auto-Post Jnl. Batch", 'Auto-Post Jnl. Batch should be set');
+ end;
+
+ [Test]
+ procedure UnitTestAutoPostJnlBatchValidateWithoutBalAccountNo()
+ var
+ ShpfyPaymentMethodMapping: Record "Shpfy Payment Method Mapping";
+ GenJournalBatch: Record "Gen. Journal Batch";
+ begin
+ // [SCENARIO] Auto-Post Jnl. Batch field validation fails when the journal batch does not have a balancing account number
+
+ // [GIVEN] A Gen. Journal Batch without a balancing account number
+ CreateJournalBatch(GenJournalBatch);
+ GenJournalBatch."Bal. Account No." := '';
+ GenJournalBatch.Modify();
+ ShpfyPaymentMethodMapping."Auto-Post Jnl. Template" := GenJournalBatch."Journal Template Name";
+
+ // [WHEN] Auto-Post Jnl. Batch is validated
+ // [THEN] Validation fails with the missing balancing-account error
+ asserterror ShpfyPaymentMethodMapping.Validate("Auto-Post Jnl. Batch", GenJournalBatch.Name);
+ LibraryAssert.ExpectedTestFieldError(GenJournalBatch.FieldCaption("Bal. Account No."), '');
+ end;
+
+ [Test]
+ procedure UnitTestAutoPostJnlBatchValidateWithEmptyValue()
+ var
+ ShpfyPaymentMethodMapping: Record "Shpfy Payment Method Mapping";
+ begin
+ // [SCENARIO] Auto-Post Jnl. Batch field can be set to empty without a validation error
+
+ // [WHEN] Auto-Post Jnl. Batch is set to empty
+ ShpfyPaymentMethodMapping.Validate("Auto-Post Jnl. Batch", '');
+
+ // [THEN] Validation passes without error
+ LibraryAssert.AreEqual('', ShpfyPaymentMethodMapping."Auto-Post Jnl. Batch", 'Auto-Post Jnl. Batch should be empty');
+ end;
+
+ [Test]
+ procedure UnitTestAutoPostRequiresJournalSetup()
+ var
+ ShpfyPaymentMethodMapping: Record "Shpfy Payment Method Mapping";
+ begin
+ // [SCENARIO] Automatic posting cannot be enabled without a configured journal template and batch
+
+ // [WHEN] Post Automatically is enabled without journal setup
+ // [THEN] Validation fails on the missing journal template
+ asserterror ShpfyPaymentMethodMapping.Validate("Post Automatically", true);
+ LibraryAssert.ExpectedTestFieldError(ShpfyPaymentMethodMapping.FieldCaption("Auto-Post Jnl. Template"), '');
+ end;
+
+ [Test]
+ procedure UnitTestPostSalesOrderWithAutoPostTransaction()
+ var
+ SalesHeader: Record "Sales Header";
+ CustLedgerEntry: Record "Cust. Ledger Entry";
+ OrderId: BigInteger;
+ TransactionId: BigInteger;
+ begin
+ // [SCENARIO] When an invoice with a Shopify Order Id is posted and Post Automatically is true, the transaction is auto-posted
+
+ // [GIVEN] Initialized test environment
+ Initialize();
+
+ // [GIVEN] A Shopify order with a transaction
+ OrderId := LibraryRandom.RandIntInRange(1000000, 1999999);
+ TransactionId := LibraryRandom.RandIntInRange(1000000, 1999999);
+ CreateShopifyOrder(OrderId);
+ CreateOrderTransaction(TransactionId, OrderId, 0, PaymentMethodMapping.Gateway, Enum::"Shpfy Transaction Type"::Sale, Item."Unit Price");
+
+ // [GIVEN] Payment method mapping with auto-post enabled
+ EnablePaymentMethodMappingAutoPost(true);
+
+ // [GIVEN] A sales invoice with a Shopify Order Id
+ CreateSalesOrder(SalesHeader, OrderId);
+
+ // [WHEN] The sales invoice is posted
+ LibrarySales.PostSalesDocument(SalesHeader, true, true);
+
+ // [THEN] A Cust. Ledger Entry is created for the transaction
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId);
+ LibraryAssert.IsFalse(CustLedgerEntry.IsEmpty(), 'Cust. Ledger Entry should be created for the auto-posted transaction');
+ end;
+
+ [Test]
+ procedure UnitTestPostSalesOrderWithAutoPostCaptureTransaction()
+ var
+ SalesHeader: Record "Sales Header";
+ CustLedgerEntry: Record "Cust. Ledger Entry";
+ OrderId: BigInteger;
+ TransactionId: BigInteger;
+ begin
+ // [SCENARIO] A successful capture transaction is automatically posted with its invoice
+ Initialize();
+
+ OrderId := LibraryRandom.RandIntInRange(13000000, 13999999);
+ TransactionId := LibraryRandom.RandIntInRange(13000000, 13999999);
+ CreateShopifyOrder(OrderId);
+ CreateOrderTransaction(TransactionId, OrderId, 0, PaymentMethodMapping.Gateway, Enum::"Shpfy Transaction Type"::Capture, Item."Unit Price");
+ EnablePaymentMethodMappingAutoPost(true);
+ CreateSalesOrder(SalesHeader, OrderId);
+
+ LibrarySales.PostSalesDocument(SalesHeader, true, true);
+
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId);
+ LibraryAssert.IsFalse(CustLedgerEntry.IsEmpty(), 'Cust. Ledger Entry should be created for the capture transaction');
+ end;
+
+ [Test]
+ procedure UnitTestPostSalesOrderWithoutAutoPostTransaction()
+ var
+ SalesHeader: Record "Sales Header";
+ CustLedgerEntry: Record "Cust. Ledger Entry";
+ OrderId: BigInteger;
+ TransactionId: BigInteger;
+ begin
+ // [SCENARIO] When an invoice with a Shopify Order Id is posted and Post Automatically is false, the transaction is not auto-posted
+
+ // [GIVEN] Initialized test environment
+ Initialize();
+
+ // [GIVEN] A Shopify order with a transaction
+ OrderId := LibraryRandom.RandIntInRange(2000000, 2999999);
+ TransactionId := LibraryRandom.RandIntInRange(2000000, 2999999);
+ CreateShopifyOrder(OrderId);
+ CreateOrderTransaction(TransactionId, OrderId, 0, PaymentMethodMapping.Gateway, Enum::"Shpfy Transaction Type"::Sale, Item."Unit Price");
+
+ // [GIVEN] Payment method mapping with auto-post disabled
+ EnablePaymentMethodMappingAutoPost(false);
+
+ // [GIVEN] A sales invoice with a Shopify Order Id
+ CreateSalesOrder(SalesHeader, OrderId);
+
+ // [WHEN] The sales invoice is posted
+ LibrarySales.PostSalesDocument(SalesHeader, true, true);
+
+ // [THEN] A Cust. Ledger Entry is not created for the transaction
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId);
+ LibraryAssert.IsTrue(CustLedgerEntry.IsEmpty(), 'Cust. Ledger Entry should not be created for the non-auto-posted transaction');
+ end;
+
+ [Test]
+ procedure UnitTestPostSalesOrderWithAutoPostMultipleTransaction()
+ var
+ SalesHeader: Record "Sales Header";
+ CustLedgerEntry: Record "Cust. Ledger Entry";
+ TransactionId1: BigInteger;
+ TransactionId2: BigInteger;
+ OrderId: BigInteger;
+ begin
+ // [SCENARIO] When an invoice with a Shopify Order Id is posted and Post Automatically is true, multiple transactions are auto-posted
+
+ // [GIVEN] Initialized test environment
+ Initialize();
+
+ // [GIVEN] A Shopify order with multiple transactions
+ OrderId := LibraryRandom.RandIntInRange(3000000, 3999999);
+ CreateShopifyOrder(OrderId);
+ TransactionId1 := LibraryRandom.RandIntInRange(3000000, 3499999);
+ TransactionId2 := LibraryRandom.RandIntInRange(3500000, 3999999);
+ CreateOrderTransaction(TransactionId1, OrderId, 0, PaymentMethodMapping.Gateway, Enum::"Shpfy Transaction Type"::Sale, Item."Unit Price" / 2);
+ CreateOrderTransaction(TransactionId2, OrderId, 0, PaymentMethodMapping.Gateway, Enum::"Shpfy Transaction Type"::Sale, Item."Unit Price" / 2);
+
+ // [GIVEN] Payment method mapping with auto-post enabled
+ EnablePaymentMethodMappingAutoPost(true);
+
+ // [GIVEN] A sales invoice with a Shopify Order Id
+ CreateSalesOrder(SalesHeader, OrderId);
+
+ // [WHEN] The sales invoice is posted
+ LibrarySales.PostSalesDocument(SalesHeader, true, true);
+
+ // [THEN] A Cust. Ledger Entry is created for each transaction
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId1);
+ LibraryAssert.IsFalse(CustLedgerEntry.IsEmpty(), 'Cust. Ledger Entry should be created for the first auto-posted transaction');
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId2);
+ LibraryAssert.IsFalse(CustLedgerEntry.IsEmpty(), 'Cust. Ledger Entry should be created for the second auto-posted transaction');
+ end;
+
+ [Test]
+ procedure UnitTestPostSalesOrderWithMixedTransactions()
+ var
+ SalesHeader: Record "Sales Header";
+ CustLedgerEntry: Record "Cust. Ledger Entry";
+ TransactionId1: BigInteger;
+ TransactionId2: BigInteger;
+ OrderId: BigInteger;
+ begin
+ // [SCENARIO] Only transactions linked to an auto-post Payment Method Mapping are auto-posted
+
+ // [GIVEN] Initialized test environment
+ Initialize();
+
+ // [GIVEN] A Shopify order
+ OrderId := LibraryRandom.RandIntInRange(4000000, 4999999);
+ CreateShopifyOrder(OrderId);
+
+ // [GIVEN] Payment method mapping with auto-post enabled
+ EnablePaymentMethodMappingAutoPost(true);
+
+ // [GIVEN] Transaction linked to an auto-post Payment Method Mapping
+ TransactionId1 := LibraryRandom.RandIntInRange(4000000, 4499999);
+ CreateOrderTransaction(TransactionId1, OrderId, 0, PaymentMethodMapping.Gateway, Enum::"Shpfy Transaction Type"::Sale, Item."Unit Price" / 2);
+
+ // [GIVEN] Transaction not linked to any Payment Method Mapping
+ TransactionId2 := LibraryRandom.RandIntInRange(4500000, 4999999);
+ CreateOrderTransaction(TransactionId2, OrderId, 0, 'auto post disabled', Enum::"Shpfy Transaction Type"::Sale, Item."Unit Price" / 2);
+
+ // [GIVEN] A sales invoice with a Shopify Order Id
+ CreateSalesOrder(SalesHeader, OrderId);
+
+ // [WHEN] The sales invoice is posted
+ LibrarySales.PostSalesDocument(SalesHeader, true, true);
+
+ // [THEN] Only the linked transaction is auto-posted
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId1);
+ LibraryAssert.IsFalse(CustLedgerEntry.IsEmpty(), 'Cust. Ledger Entry should be created for the auto-posted transaction');
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId2);
+ LibraryAssert.IsTrue(CustLedgerEntry.IsEmpty(), 'Cust. Ledger Entry should not be created for the non-linked transaction');
+ end;
+
+ [Test]
+ procedure UnitTestPostCreditMemoWithAutoPostTransaction()
+ var
+ SalesHeader: Record "Sales Header";
+ CustLedgerEntry: Record "Cust. Ledger Entry";
+ TransactionId: BigInteger;
+ RefundId: BigInteger;
+ OrderId: BigInteger;
+ begin
+ // [SCENARIO] When a credit memo with a Shopify Refund Id is posted and Post Automatically is true, the refund transaction is auto-posted
+
+ // [GIVEN] Initialized test environment
+ Initialize();
+
+ // [GIVEN] A refund with a transaction
+ RefundId := LibraryRandom.RandIntInRange(5000000, 5999999);
+ OrderId := LibraryRandom.RandIntInRange(5000000, 5999999);
+ CreateShopifyOrder(OrderId);
+ CreateRefund(RefundId, OrderId);
+ TransactionId := LibraryRandom.RandIntInRange(5000000, 5999999);
+ CreateOrderTransaction(TransactionId, OrderId, RefundId, PaymentMethodMapping.Gateway, Enum::"Shpfy Transaction Type"::Refund, Item."Unit Price");
+
+ // [GIVEN] Payment method mapping with auto-post enabled
+ EnablePaymentMethodMappingAutoPost(true);
+
+ // [GIVEN] A sales credit memo with a Shopify Refund Id
+ CreateCreditMemo(SalesHeader, RefundId);
+
+ // [WHEN] The credit memo is posted
+ LibrarySales.PostSalesDocument(SalesHeader, true, true);
+
+ // [THEN] A Cust. Ledger Entry is created for the refund transaction
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId);
+ LibraryAssert.IsFalse(CustLedgerEntry.IsEmpty(), 'Cust. Ledger Entry should be created for the auto-posted refund transaction');
+ end;
+
+ [Test]
+ procedure UnitTestAutoPostWorksWithPostWithJobQueueEnabled()
+ var
+ GeneralLedgerSetup: Record "General Ledger Setup";
+ SalesHeader: Record "Sales Header";
+ CustLedgerEntry: Record "Cust. Ledger Entry";
+ OrderId: BigInteger;
+ TransactionId: BigInteger;
+ OriginalPostWithJobQueue: Boolean;
+ begin
+ // [SCENARIO] Automatic posting is synchronous and works even when "Post with Job Queue" is enabled in the General Ledger Setup
+
+ // [GIVEN] Initialized test environment
+ Initialize();
+
+ // [GIVEN] Post with Job Queue is enabled
+ GeneralLedgerSetup.Get();
+ OriginalPostWithJobQueue := GeneralLedgerSetup."Post with Job Queue";
+ GeneralLedgerSetup."Post with Job Queue" := true;
+ GeneralLedgerSetup.Modify();
+
+ // [GIVEN] A Shopify order with a transaction linked to an auto-post mapping
+ OrderId := LibraryRandom.RandIntInRange(7000000, 7999999);
+ TransactionId := LibraryRandom.RandIntInRange(7000000, 7999999);
+ CreateShopifyOrder(OrderId);
+ CreateOrderTransaction(TransactionId, OrderId, 0, PaymentMethodMapping.Gateway, Enum::"Shpfy Transaction Type"::Sale, Item."Unit Price");
+ EnablePaymentMethodMappingAutoPost(true);
+
+ // [GIVEN] A sales invoice with a Shopify Order Id
+ CreateSalesOrder(SalesHeader, OrderId);
+
+ // [WHEN] The sales invoice is posted
+ LibrarySales.PostSalesDocument(SalesHeader, true, true);
+
+ // [THEN] The transaction is posted synchronously (Cust. Ledger Entry exists), it is not scheduled to the job queue
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId);
+ LibraryAssert.IsFalse(CustLedgerEntry.IsEmpty(), 'Transaction should be posted synchronously even with Post with Job Queue enabled');
+
+ // Restore the setup (harmless either way, as automatic posting never uses the job queue).
+ GeneralLedgerSetup.Get();
+ GeneralLedgerSetup."Post with Job Queue" := OriginalPostWithJobQueue;
+ GeneralLedgerSetup.Modify();
+ end;
+
+ [Test]
+ procedure UnitTestPostSalesOrderFailedTransactionIsBestEffort()
+ var
+ SalesHeader: Record "Sales Header";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ CustLedgerEntry: Record "Cust. Ledger Entry";
+ SkippedRecord: Record "Shpfy Skipped Record";
+ OrderTransaction: Record "Shpfy Order Transaction";
+ FailingGateway: Text[30];
+ OrderId: BigInteger;
+ TransactionId: BigInteger;
+ begin
+ // [SCENARIO] When automatic posting of a transaction fails, the document posting still succeeds and the failure is logged
+
+ // [GIVEN] Initialized test environment
+ Initialize();
+
+ // [GIVEN] A payment method mapping whose journal batch cannot post (no number series)
+ FailingGateway := CreateFailingPaymentMethodMapping();
+
+ // [GIVEN] A Shopify order with a transaction using the failing mapping
+ OrderId := LibraryRandom.RandIntInRange(6000000, 6999999);
+ TransactionId := LibraryRandom.RandIntInRange(6000000, 6999999);
+ CreateShopifyOrder(OrderId);
+ CreateOrderTransaction(TransactionId, OrderId, 0, FailingGateway, Enum::"Shpfy Transaction Type"::Sale, Item."Unit Price");
+
+ // [GIVEN] A sales invoice with a Shopify Order Id
+ CreateSalesOrder(SalesHeader, OrderId);
+
+ // [WHEN] The sales invoice is posted
+ LibrarySales.PostSalesDocument(SalesHeader, true, true);
+
+ // [THEN] The sales invoice is posted (document posting is not blocked by the payment failure)
+ SalesInvoiceHeader.SetRange("Shpfy Order Id", OrderId);
+ LibraryAssert.IsFalse(SalesInvoiceHeader.IsEmpty(), 'Posted sales invoice should exist');
+
+ // [THEN] No Cust. Ledger Entry is created for the failed transaction
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId);
+ LibraryAssert.IsTrue(CustLedgerEntry.IsEmpty(), 'Cust. Ledger Entry should not be created for the failed transaction');
+
+ // [THEN] No orphaned general journal line is left behind
+ LibraryAssert.IsTrue(NoJournalLineExistsForTransaction(TransactionId), 'No general journal line should be left behind for the failed transaction');
+
+ // [THEN] The failure is logged as a skipped record
+ OrderTransaction.Get(TransactionId);
+ SkippedRecord.SetRange("Record ID", OrderTransaction.RecordId);
+ LibraryAssert.IsFalse(SkippedRecord.IsEmpty(), 'A skipped record should be logged for the failed transaction');
+ end;
+
+ [Test]
+ procedure UnitTestPreviewSalesOrderDoesNotAutoPost()
+ var
+ SalesHeader: Record "Sales Header";
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ GLPostingPreview: TestPage "G/L Posting Preview";
+ OrderId: BigInteger;
+ TransactionId: BigInteger;
+ begin
+ // [SCENARIO] Previewing the posting of an invoice does not automatically post the transaction and does not break the preview
+
+ // [GIVEN] Initialized test environment
+ Initialize();
+
+ // [GIVEN] A Shopify order with a transaction linked to an auto-post mapping
+ OrderId := LibraryRandom.RandIntInRange(8000000, 8999999);
+ TransactionId := LibraryRandom.RandIntInRange(8000000, 8999999);
+ CreateShopifyOrder(OrderId);
+ CreateOrderTransaction(TransactionId, OrderId, 0, PaymentMethodMapping.Gateway, Enum::"Shpfy Transaction Type"::Sale, Item."Unit Price");
+ EnablePaymentMethodMappingAutoPost(true);
+
+ // [GIVEN] A sales invoice with a Shopify Order Id
+ CreateSalesOrder(SalesHeader, OrderId);
+ Commit();
+
+ // [WHEN] The posting of the sales invoice is previewed
+ GLPostingPreview.Trap();
+ asserterror LibrarySales.PreviewPostSalesDocument(SalesHeader);
+
+ // [THEN] The preview completes without a real error (auto-posting did not run and did not break the preview)
+ LibraryAssert.AreEqual('', GetLastErrorText(), 'Posting preview should not raise a real error');
+ GLPostingPreview.Close();
+
+ // [THEN] Nothing was actually posted
+ SalesInvoiceHeader.SetRange("Shpfy Order Id", OrderId);
+ LibraryAssert.IsTrue(SalesInvoiceHeader.IsEmpty(), 'Preview should not post the sales invoice');
+ end;
+
+ [Test]
+ procedure UnitTestAutoPostDoesNotPostUnrelatedBatchLines()
+ var
+ SalesHeader: Record "Sales Header";
+ CustLedgerEntry: Record "Cust. Ledger Entry";
+ UnrelatedDocNo: Code[20];
+ OrderId: BigInteger;
+ TransactionId: BigInteger;
+ begin
+ // [SCENARIO] An unrelated line parked in the configured auto-post batch is not posted when a Shopify invoice auto-posts
+
+ // [GIVEN] Initialized test environment
+ Initialize();
+
+ // [GIVEN] An unrelated manual journal line sitting in the configured auto-post batch
+ UnrelatedDocNo := CreateUnrelatedJournalLine();
+
+ // [GIVEN] A Shopify order with an auto-post-enabled transaction
+ OrderId := LibraryRandom.RandIntInRange(9000000, 9499999);
+ TransactionId := LibraryRandom.RandIntInRange(9000000, 9499999);
+ CreateShopifyOrder(OrderId);
+ CreateOrderTransaction(TransactionId, OrderId, 0, PaymentMethodMapping.Gateway, Enum::"Shpfy Transaction Type"::Sale, Item."Unit Price");
+ EnablePaymentMethodMappingAutoPost(true);
+
+ // [GIVEN] A sales invoice with a Shopify Order Id
+ CreateSalesOrder(SalesHeader, OrderId);
+
+ // [WHEN] The sales invoice is posted
+ LibrarySales.PostSalesDocument(SalesHeader, true, true);
+
+ // [THEN] The Shopify transaction is auto-posted
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId);
+ LibraryAssert.IsFalse(CustLedgerEntry.IsEmpty(), 'Cust. Ledger Entry should be created for the auto-posted transaction');
+
+ // [THEN] The unrelated journal line is untouched (still present, not posted)
+ LibraryAssert.IsTrue(UnrelatedJournalLineExists(UnrelatedDocNo), 'The unrelated journal line in the configured batch must not be posted');
+ end;
+
+ [Test]
+ procedure UnitTestAutoPostAfterDocumentLinkCreation()
+ var
+ SalesHeader: Record "Sales Header";
+ CustLedgerEntry: Record "Cust. Ledger Entry";
+ OrderId: BigInteger;
+ TransactionId: BigInteger;
+ begin
+ // [SCENARIO] Document-link writes are committed before the isolated automatic-posting operation runs
+ Initialize();
+
+ OrderId := LibraryRandom.RandIntInRange(14000000, 14999999);
+ TransactionId := LibraryRandom.RandIntInRange(14000000, 14999999);
+ CreateShopifyOrder(OrderId);
+ CreateOrderTransaction(TransactionId, OrderId, 0, PaymentMethodMapping.Gateway, Enum::"Shpfy Transaction Type"::Sale, Item."Unit Price");
+ EnablePaymentMethodMappingAutoPost(true);
+ CreateSalesOrderDocument(SalesHeader, OrderId);
+ CreateShopifyOrderDocumentLink(SalesHeader, OrderId);
+
+ LibrarySales.PostSalesDocument(SalesHeader, true, true);
+
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId);
+ LibraryAssert.IsFalse(CustLedgerEntry.IsEmpty(), 'Document-link creation must not prevent the transaction from being auto-posted');
+ end;
+
+ [Test]
+ procedure UnitTestAutoPostDefersWhilePartialInvoiceOpen()
+ var
+ SalesHeaderToPost: Record "Sales Header";
+ SalesHeaderOpen: Record "Sales Header";
+ CustLedgerEntry: Record "Cust. Ledger Entry";
+ OrderId: BigInteger;
+ TransactionId: BigInteger;
+ begin
+ // [SCENARIO] The order transaction is not consumed while another, not-yet-posted invoice exists for the same order
+
+ // [GIVEN] Initialized test environment
+ Initialize();
+
+ // [GIVEN] A Shopify order with an auto-post-enabled transaction that covers both invoices
+ OrderId := LibraryRandom.RandIntInRange(9500000, 9999999);
+ TransactionId := LibraryRandom.RandIntInRange(9500000, 9999999);
+ CreateShopifyOrder(OrderId);
+ CreateOrderTransaction(TransactionId, OrderId, 0, PaymentMethodMapping.Gateway, Enum::"Shpfy Transaction Type"::Sale, 2 * Item."Unit Price");
+ EnablePaymentMethodMappingAutoPost(true);
+
+ // [GIVEN] Two sales invoices for the same Shopify order
+ CreateSalesOrder(SalesHeaderOpen, OrderId);
+ CreateSalesOrder(SalesHeaderToPost, OrderId);
+
+ // [WHEN] The first invoice is posted while the second is still open
+ LibrarySales.PostSalesDocument(SalesHeaderToPost, true, true);
+
+ // [THEN] Auto-posting is deferred - the transaction is not consumed yet
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId);
+ LibraryAssert.IsTrue(CustLedgerEntry.IsEmpty(), 'Transaction should not be auto-posted while another invoice for the order is still open');
+
+ // [WHEN] The remaining invoice is posted
+ LibrarySales.PostSalesDocument(SalesHeaderOpen, true, true);
+
+ // [THEN] The transaction is now auto-posted
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId);
+ LibraryAssert.IsFalse(CustLedgerEntry.IsEmpty(), 'Transaction should be auto-posted once no open invoice remains for the order.');
+ end;
+
+ [Test]
+ procedure UnitTestSetJournalParametersPropagatesToGeneratedLine()
+ var
+ GenJournalLine: Record "Gen. Journal Line";
+ OrderTransaction: Record "Shpfy Order Transaction";
+ SalesHeader: Record "Sales Header";
+ SuggestPayments: Report "Shpfy Suggest Payments";
+ PostedInvoiceNo: Code[20];
+ OrderId: BigInteger;
+ TransactionId: BigInteger;
+ begin
+ // [SCENARIO] SetJournalParameters carries the mapped template, batch, and posting date onto the generated journal line
+
+ // [GIVEN] Initialized test environment with a posted Shopify invoice and transaction
+ Initialize();
+ // Auto-post must stay off here so the transaction is left for the manual suggest-payments call below.
+ EnablePaymentMethodMappingAutoPost(false);
+ OrderId := LibraryRandom.RandIntInRange(10000000, 10999999);
+ TransactionId := LibraryRandom.RandIntInRange(10000000, 10999999);
+ CreateShopifyOrder(OrderId);
+ CreateOrderTransaction(TransactionId, OrderId, 0, PaymentMethodMapping.Gateway, Enum::"Shpfy Transaction Type"::Sale, Item."Unit Price");
+ CreateSalesOrder(SalesHeader, OrderId);
+ PostedInvoiceNo := LibrarySales.PostSalesDocument(SalesHeader, true, true);
+
+ // [WHEN] The suggest-payments report generates lines with explicit journal parameters
+ OrderTransaction.Get(TransactionId);
+ SuggestPayments.SetJournalParameters(PaymentMethodMapping."Auto-Post Jnl. Template", PaymentMethodMapping."Auto-Post Jnl. Batch", WorkDate());
+ SuggestPayments.GetOrderTransactions(OrderTransaction);
+ SuggestPayments.CreateGeneralJournalLines();
+
+ // [THEN] The generated line uses the mapped template, batch, and posting date
+ GenJournalLine.SetRange("Shpfy Transaction Id", TransactionId);
+ LibraryAssert.IsTrue(GenJournalLine.FindFirst(), 'A general journal line should be generated for the transaction');
+ LibraryAssert.AreEqual(PaymentMethodMapping."Auto-Post Jnl. Template", GenJournalLine."Journal Template Name", 'Journal template should match the mapping');
+ LibraryAssert.AreEqual(PaymentMethodMapping."Auto-Post Jnl. Batch", GenJournalLine."Journal Batch Name", 'Journal batch should match the mapping');
+ LibraryAssert.AreEqual(WorkDate(), GenJournalLine."Posting Date", 'Posting date should match the value passed to SetJournalParameters');
+ LibraryAssert.AreEqual(PostedInvoiceNo, GenJournalLine."Applies-to Doc. No.", 'Generated payment line should apply to the posted invoice');
+
+ // Clean up the unposted lines so they don't leak into other tests.
+ GenJournalLine.SetRange("Shpfy Transaction Id", TransactionId);
+ GenJournalLine.DeleteAll(true);
+ end;
+
+ [Test]
+ procedure UnitTestAutoPostWithFuturePostingDateIsHeadless()
+ var
+ SalesHeader: Record "Sales Header";
+ CustLedgerEntry: Record "Cust. Ledger Entry";
+ PostingDate: Date;
+ OrderId: BigInteger;
+ TransactionId: BigInteger;
+ begin
+ // [SCENARIO] Automatic posting pre-confirms the journal posting-date prompt
+ Initialize();
+
+ PostingDate := CalcDate('<1D>', WorkDate());
+ OrderId := LibraryRandom.RandIntInRange(15000000, 15999999);
+ TransactionId := LibraryRandom.RandIntInRange(15000000, 15999999);
+ CreateShopifyOrder(OrderId);
+ CreateOrderTransaction(TransactionId, OrderId, 0, PaymentMethodMapping.Gateway, Enum::"Shpfy Transaction Type"::Sale, Item."Unit Price");
+ EnablePaymentMethodMappingAutoPost(true);
+ CreateSalesOrder(SalesHeader, OrderId);
+ SalesHeader.Validate("Posting Date", PostingDate);
+ SalesHeader.Modify(true);
+ EnablePostingAfterWorkingDateConfirmation();
+
+ LibrarySales.PostSalesDocument(SalesHeader, true, true);
+
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId);
+ LibraryAssert.IsFalse(CustLedgerEntry.IsEmpty(), 'The transaction should be auto-posted without a confirmation dialog');
+ DisablePostingAfterWorkingDateConfirmation();
+ end;
+
+ [Test]
+ procedure UnitTestAutoPostSkippedWhenCommitSuppressed()
+ var
+ SalesHeader: Record "Sales Header";
+ CustLedgerEntry: Record "Cust. Ledger Entry";
+ SkippedRecord: Record "Shpfy Skipped Record";
+ SalesPost: Codeunit "Sales-Post";
+ OrderId: BigInteger;
+ TransactionId: BigInteger;
+ begin
+ // [SCENARIO] Auto-posting is skipped when the caller suppresses commit and owns the transaction
+
+ // [GIVEN] Initialized test environment
+ Initialize();
+
+ // [GIVEN] A Shopify order with an auto-post-enabled transaction and a matching sales invoice
+ OrderId := LibraryRandom.RandIntInRange(11000000, 11999999);
+ TransactionId := LibraryRandom.RandIntInRange(11000000, 11999999);
+ CreateShopifyOrder(OrderId);
+ CreateOrderTransaction(TransactionId, OrderId, 0, PaymentMethodMapping.Gateway, Enum::"Shpfy Transaction Type"::Sale, Item."Unit Price");
+ EnablePaymentMethodMappingAutoPost(true);
+ CreateSalesOrder(SalesHeader, OrderId);
+
+ // [WHEN] The sales invoice is posted with commit suppressed (the caller owns the transaction)
+ SalesHeader.Ship := true;
+ SalesHeader.Invoice := true;
+ SalesHeader.Modify();
+ SalesPost.SetSuppressCommit(true);
+ SalesPost.Run(SalesHeader);
+
+ // [THEN] Auto-posting did not run: no ledger entry and no skipped record for the transaction
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId);
+ LibraryAssert.IsTrue(CustLedgerEntry.IsEmpty(), 'Auto-posting must not run when commit is suppressed');
+ SkippedRecord.SetRange("Shopify Id", TransactionId);
+ LibraryAssert.IsTrue(SkippedRecord.IsEmpty(), 'No skipped record should be logged when auto-posting is skipped');
+ end;
+
+ [Test]
+ procedure UnitTestAutoPostDefersWhilePartialCreditMemoOpen()
+ var
+ SalesHeaderToPost: Record "Sales Header";
+ SalesHeaderOpen: Record "Sales Header";
+ CustLedgerEntry: Record "Cust. Ledger Entry";
+ OrderId: BigInteger;
+ RefundId: BigInteger;
+ TransactionId: BigInteger;
+ begin
+ // [SCENARIO] The refund transaction is not consumed while another, not-yet-posted credit memo exists for the same refund
+
+ // [GIVEN] Initialized test environment
+ Initialize();
+
+ // [GIVEN] A Shopify refund with an auto-post-enabled transaction that covers both credit memos
+ OrderId := LibraryRandom.RandIntInRange(12000000, 12999999);
+ RefundId := LibraryRandom.RandIntInRange(12000000, 12999999);
+ TransactionId := LibraryRandom.RandIntInRange(12000000, 12999999);
+ CreateShopifyOrder(OrderId);
+ CreateRefund(RefundId, OrderId);
+ CreateOrderTransaction(TransactionId, OrderId, RefundId, PaymentMethodMapping.Gateway, Enum::"Shpfy Transaction Type"::Refund, 2 * Item."Unit Price");
+ EnablePaymentMethodMappingAutoPost(true);
+
+ // [GIVEN] Two credit memos for the same Shopify refund
+ CreateCreditMemo(SalesHeaderOpen, RefundId);
+ CreateCreditMemo(SalesHeaderToPost, RefundId);
+
+ // [WHEN] The first credit memo is posted while the second is still open
+ LibrarySales.PostSalesDocument(SalesHeaderToPost, true, true);
+
+ // [THEN] Auto-posting is deferred - the transaction is not consumed yet
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId);
+ LibraryAssert.IsTrue(CustLedgerEntry.IsEmpty(), 'Refund transaction should not be auto-posted while another credit memo for the refund is still open');
+
+ // [WHEN] The remaining credit memo is posted
+ LibrarySales.PostSalesDocument(SalesHeaderOpen, true, true);
+
+ // [THEN] The transaction is now auto-posted
+ CustLedgerEntry.SetRange("Shpfy Transaction Id", TransactionId);
+ LibraryAssert.IsFalse(CustLedgerEntry.IsEmpty(), 'Refund transaction should be auto-posted once no open credit memo remains for the refund.');
+ end;
+
+ [Test]
+ procedure UnitTestPostableEligibilityExcludesOpenSalesDocument()
+ var
+ SalesHeaderOpen: Record "Sales Header";
+ SalesHeaderToPost: Record "Sales Header";
+ OrderTransaction: Record "Shpfy Order Transaction";
+ PaymentMethodMappingForEligibility: Record "Shpfy Payment Method Mapping";
+ AutoPostEligibility: Codeunit "Shpfy Auto Post Eligibility";
+ OrderId: BigInteger;
+ TransactionId: BigInteger;
+ begin
+ // [SCENARIO] The postable-transactions predicate matches automatic posting's partial-invoice deferral
+ Initialize();
+
+ OrderId := LibraryRandom.RandIntInRange(16000000, 16999999);
+ TransactionId := LibraryRandom.RandIntInRange(16000000, 16999999);
+ CreateShopifyOrder(OrderId);
+ CreateOrderTransaction(TransactionId, OrderId, 0, PaymentMethodMapping.Gateway, Enum::"Shpfy Transaction Type"::Sale, 2 * Item."Unit Price");
+ EnablePaymentMethodMappingAutoPost(false);
+ CreateSalesOrder(SalesHeaderOpen, OrderId);
+ CreateSalesOrder(SalesHeaderToPost, OrderId);
+ LibrarySales.PostSalesDocument(SalesHeaderToPost, true, true);
+ EnablePaymentMethodMappingAutoPost(true);
+ OrderTransaction.Get(TransactionId);
+
+ LibraryAssert.IsFalse(
+ AutoPostEligibility.IsReadyToPost(OrderTransaction, PaymentMethodMappingForEligibility),
+ 'A transaction must not be shown as postable while another sales document for its order is open');
+
+ SalesHeaderOpen.Delete(true);
+ LibraryAssert.IsTrue(
+ AutoPostEligibility.IsReadyToPost(OrderTransaction, PaymentMethodMappingForEligibility),
+ 'A transaction should be postable after its invoice is posted and no related sales document remains open');
+ end;
+
+ local procedure Initialize()
+ var
+ LibraryERMCountryData: Codeunit "Library - ERM Country Data";
+ CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
+ begin
+ if IsInitialized then
+ exit;
+
+ Codeunit.Run(Codeunit::"Shpfy Initialize Test");
+
+ LibraryERMCountryData.CreateVATData();
+ LibraryERMCountryData.UpdateGeneralPostingSetup();
+ CreateItem();
+ LibrarySales.CreateCustomer(Customer);
+
+ Shop := CommunicationMgt.GetShopRecord();
+ Shop."Logging Mode" := Shop."Logging Mode"::"Error Only";
+ Shop.Modify();
+
+ CreatePaymentMethodMapping();
+
+ IsInitialized := true;
+ end;
+
+ local procedure CreateItem()
+ var
+ LibraryInventory: Codeunit "Library - Inventory";
+ Amount: Decimal;
+ begin
+ Amount := LibraryRandom.RandIntInRange(10000, 99999);
+ // A service item is used so posting the sales invoice does not require Inventory Posting Setup;
+ // the feature only depends on the resulting customer ledger entry, not on inventory posting.
+ LibraryInventory.CreateItem(Item);
+ Item.Validate(Type, Item.Type::Service);
+ Item.Validate("Unit Price", Amount);
+ Item.Modify(true);
+ end;
+
+ local procedure CreateShopifyOrder(OrderId: BigInteger)
+ var
+ ShpfyOrderHeader: Record "Shpfy Order Header";
+ begin
+ ShpfyOrderHeader.Init();
+ ShpfyOrderHeader."Shopify Order Id" := OrderId;
+ ShpfyOrderHeader.Processed := true;
+ ShpfyOrderHeader.Insert();
+ end;
+
+ local procedure CreateOrderTransaction(TransactionId: BigInteger; OrderId: BigInteger; RefundId: BigInteger; Gateway: Text[30]; TransactionType: Enum "Shpfy Transaction Type"; Amount: Decimal)
+ var
+ OrderTransaction: Record "Shpfy Order Transaction";
+ begin
+ OrderTransaction.Init();
+ OrderTransaction."Shopify Transaction Id" := TransactionId;
+ OrderTransaction."Shopify Order Id" := OrderId;
+ OrderTransaction."Refund Id" := RefundId;
+ OrderTransaction.Shop := Shop.Code;
+ OrderTransaction.Gateway := Gateway;
+ OrderTransaction.Type := TransactionType;
+ OrderTransaction.Status := OrderTransaction.Status::Success;
+ OrderTransaction.Amount := Amount;
+ OrderTransaction.Insert();
+ end;
+
+ local procedure CreateSalesOrder(var SalesHeader: Record "Sales Header"; OrderId: BigInteger)
+ var
+ SalesLine: Record "Sales Line";
+ begin
+ LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, Customer."No.");
+ SalesHeader."Shpfy Order Id" := OrderId;
+ SalesHeader.Modify();
+ LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, Item."No.", 1);
+ end;
+
+ local procedure CreateSalesOrderDocument(var SalesHeader: Record "Sales Header"; OrderId: BigInteger)
+ var
+ SalesLine: Record "Sales Line";
+ begin
+ LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Order, Customer."No.");
+ SalesHeader."Shpfy Order Id" := OrderId;
+ SalesHeader.Modify();
+ LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, Item."No.", 1);
+ end;
+
+ local procedure CreateShopifyOrderDocumentLink(SalesHeader: Record "Sales Header"; OrderId: BigInteger)
+ var
+ DocLinkToBCDoc: Record "Shpfy Doc. Link To Doc.";
+ begin
+ DocLinkToBCDoc."Shopify Document Type" := DocLinkToBCDoc."Shopify Document Type"::"Shopify Shop Order";
+ DocLinkToBCDoc."Shopify Document Id" := OrderId;
+ DocLinkToBCDoc."Document Type" := DocLinkToBCDoc."Document Type"::"Sales Order";
+ DocLinkToBCDoc."Document No." := SalesHeader."No.";
+ DocLinkToBCDoc.Insert();
+ end;
+
+ local procedure CreateCreditMemo(var SalesHeader: Record "Sales Header"; RefundId: BigInteger)
+ var
+ SalesLine: Record "Sales Line";
+ begin
+ LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::"Credit Memo", Customer."No.");
+ SalesHeader."Shpfy Refund Id" := RefundId;
+ SalesHeader.Modify();
+ LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, Item."No.", 1);
+ end;
+
+ local procedure CreateRefund(RefundId: BigInteger; OrderId: BigInteger)
+ var
+ RefundHeader: Record "Shpfy Refund Header";
+ begin
+ RefundHeader.Init();
+ RefundHeader."Refund Id" := RefundId;
+ RefundHeader."Order Id" := OrderId;
+ RefundHeader.Insert();
+ end;
+
+ local procedure EnablePaymentMethodMappingAutoPost(AutoPost: Boolean)
+ begin
+ PaymentMethodMapping."Post Automatically" := AutoPost;
+ PaymentMethodMapping.Modify();
+ end;
+
+ local procedure CreatePaymentMethodMapping()
+ var
+ GenJournalBatch: Record "Gen. Journal Batch";
+ begin
+ PaymentMethodMapping.Init();
+ PaymentMethodMapping."Shop Code" := Shop.Code;
+ PaymentMethodMapping.Gateway := CopyStr(LibraryRandom.RandText(30), 1, MaxStrLen(PaymentMethodMapping.Gateway));
+ PaymentMethodMapping."Post Automatically" := true;
+ CreateJournalBatch(GenJournalBatch);
+ PaymentMethodMapping."Auto-Post Jnl. Template" := GenJournalBatch."Journal Template Name";
+ PaymentMethodMapping."Auto-Post Jnl. Batch" := GenJournalBatch.Name;
+ PaymentMethodMapping.Insert();
+ end;
+
+ local procedure CreateFailingPaymentMethodMapping(): Text[30]
+ var
+ FailingMapping: Record "Shpfy Payment Method Mapping";
+ GenJournalBatch: Record "Gen. Journal Batch";
+ FailingGateway: Text[30];
+ begin
+ // A batch with a balancing account but without a number series: journal lines get no document number
+ // and posting therefore fails, which is used to exercise the best-effort failure handling.
+ CreateJournalBatch(GenJournalBatch);
+ GenJournalBatch.Validate("No. Series", '');
+ GenJournalBatch.Modify(true);
+
+ FailingGateway := CopyStr(LibraryRandom.RandText(30), 1, MaxStrLen(FailingMapping.Gateway));
+ FailingMapping."Shop Code" := Shop.Code;
+ FailingMapping.Gateway := FailingGateway;
+ FailingMapping."Post Automatically" := true;
+ FailingMapping."Auto-Post Jnl. Template" := GenJournalBatch."Journal Template Name";
+ FailingMapping."Auto-Post Jnl. Batch" := GenJournalBatch.Name;
+ FailingMapping.Insert();
+ exit(FailingGateway);
+ end;
+
+ local procedure CreateJournalBatch(var GenJournalBatch: Record "Gen. Journal Batch")
+ var
+ GenJournalTemplate: Record "Gen. Journal Template";
+ SourceCode: Record "Source Code";
+ begin
+ LibraryERM.CreateSourceCode(SourceCode);
+ LibraryERM.CreateGenJournalTemplate(GenJournalTemplate);
+ GenJournalTemplate.Validate(Type, GenJournalTemplate.Type::"Cash Receipts");
+ GenJournalTemplate.Validate("Source Code", SourceCode.Code);
+ GenJournalTemplate.Modify(true);
+ LibraryERM.CreateGenJournalBatch(GenJournalBatch, GenJournalTemplate.Name);
+ GenJournalBatch.Validate("Bal. Account Type", GenJournalBatch."Bal. Account Type"::"G/L Account");
+ GenJournalBatch.Validate("Bal. Account No.", CreateGLAccount());
+ GenJournalBatch.Validate("No. Series", LibraryERM.CreateNoSeriesCode());
+ GenJournalBatch.Modify(true);
+ end;
+
+ local procedure CreateGLAccount(): Code[20]
+ var
+ GLAccount: Record "G/L Account";
+ begin
+ LibraryERM.CreateGLAccount(GLAccount);
+ GLAccount.Validate("Direct Posting", true);
+ GLAccount.Modify(true);
+ exit(GLAccount."No.");
+ end;
+
+ local procedure EnablePostingAfterWorkingDateConfirmation()
+ var
+ AccountingPeriod: Record "Accounting Period";
+ MyNotifications: Record "My Notifications";
+ InstructionMgt: Codeunit "Instruction Mgt.";
+ begin
+ if AccountingPeriod.IsEmpty() then begin
+ AccountingPeriod."Starting Date" := WorkDate();
+ AccountingPeriod.Insert();
+ end;
+ MyNotifications.InsertDefault(
+ InstructionMgt.GetPostingAfterWorkingDateNotificationId(),
+ InstructionMgt.PostingAfterWorkingDateNotAllowedCode(),
+ '', true);
+ end;
+
+ local procedure DisablePostingAfterWorkingDateConfirmation()
+ var
+ MyNotifications: Record "My Notifications";
+ InstructionMgt: Codeunit "Instruction Mgt.";
+ begin
+ if MyNotifications.Get(UserId(), InstructionMgt.GetPostingAfterWorkingDateNotificationId()) then
+ MyNotifications.Delete();
+ end;
+
+ local procedure NoJournalLineExistsForTransaction(TransactionId: BigInteger): Boolean
+ var
+ GenJournalLine: Record "Gen. Journal Line";
+ begin
+ GenJournalLine.SetRange("Shpfy Transaction Id", TransactionId);
+ exit(GenJournalLine.IsEmpty());
+ end;
+
+ local procedure CreateUnrelatedJournalLine(): Code[20]
+ var
+ GenJournalLine: Record "Gen. Journal Line";
+ GenJournalBatch: Record "Gen. Journal Batch";
+ NoSeries: Codeunit "No. Series";
+ DocNo: Code[20];
+ LastLineNo: Integer;
+ begin
+ // A self-balancing line parked in the configured batch; it would post if the whole batch posted.
+ GenJournalBatch.Get(PaymentMethodMapping."Auto-Post Jnl. Template", PaymentMethodMapping."Auto-Post Jnl. Batch");
+ DocNo := NoSeries.PeekNextNo(GenJournalBatch."No. Series", WorkDate());
+
+ GenJournalLine.SetRange("Journal Template Name", GenJournalBatch."Journal Template Name");
+ GenJournalLine.SetRange("Journal Batch Name", GenJournalBatch.Name);
+ if GenJournalLine.FindLast() then
+ LastLineNo := GenJournalLine."Line No.";
+
+ GenJournalLine.Init();
+ GenJournalLine."Journal Template Name" := GenJournalBatch."Journal Template Name";
+ GenJournalLine."Journal Batch Name" := GenJournalBatch.Name;
+ GenJournalLine."Line No." := LastLineNo + 10000;
+ GenJournalLine.Validate("Posting Date", WorkDate());
+ GenJournalLine."Document No." := DocNo;
+ GenJournalLine.Validate("Account Type", GenJournalLine."Account Type"::"G/L Account");
+ GenJournalLine.Validate("Account No.", CreateGLAccount());
+ GenJournalLine.Validate(Amount, LibraryRandom.RandDecInRange(100, 1000, 2));
+ GenJournalLine.Validate("Bal. Account Type", GenJournalLine."Bal. Account Type"::"G/L Account");
+ GenJournalLine.Validate("Bal. Account No.", CreateGLAccount());
+ GenJournalLine.Insert(true);
+ exit(DocNo);
+ end;
+
+ local procedure UnrelatedJournalLineExists(DocNo: Code[20]): Boolean
+ var
+ GenJournalLine: Record "Gen. Journal Line";
+ begin
+ GenJournalLine.SetRange("Journal Template Name", PaymentMethodMapping."Auto-Post Jnl. Template");
+ GenJournalLine.SetRange("Journal Batch Name", PaymentMethodMapping."Auto-Post Jnl. Batch");
+ GenJournalLine.SetRange("Document No.", DocNo);
+ exit(not GenJournalLine.IsEmpty());
+ end;
+}
diff --git a/src/Apps/W1/Shopify/Test/app.json b/src/Apps/W1/Shopify/Test/app.json
index 16b2c8826a4..7c3caa0dc67 100644
--- a/src/Apps/W1/Shopify/Test/app.json
+++ b/src/Apps/W1/Shopify/Test/app.json
@@ -52,6 +52,10 @@
"screenshots": [],
"platform": "29.0.0.0",
"idRanges": [
+ {
+ "from": 139415,
+ "to": 139420
+ },
{
"from": 134241,
"to": 134247