Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6ade014
[Shopify] Automatic Transaction Posting
onbuyuka Jul 16, 2026
43ccc94
Merge branch 'main' of https://github.com/microsoft/BCApps into bugs/…
onbuyuka Jul 16, 2026
9fc5f4a
Merge branch 'main' of https://github.com/microsoft/BCApps into bugs/…
onbuyuka Jul 23, 2026
a1dc4d7
Merge branch 'main' of https://github.com/microsoft/BCApps into bugs/…
onbuyuka Aug 10, 2026
34eab3e
Address PR review feedback on Shopify automatic transaction posting
onbuyuka Aug 24, 2026
bd28a61
Fix clashing test object ID: use 139415 for Shpfy Auto Post Trans. Test
onbuyuka Aug 24, 2026
f4b58b5
Address review feedback: perf, style and test coverage
onbuyuka Aug 24, 2026
2f9d707
Make auto-post test fixture self-contained to fix CI posting setup fa…
onbuyuka Aug 25, 2026
b77c22f
Seed number series in auto-post test so first-run shop init succeeds
onbuyuka Aug 25, 2026
addd2a6
Revert "Seed number series in auto-post test so first-run shop init s…
onbuyuka Aug 25, 2026
97bba5e
Revert "Make auto-post test fixture self-contained to fix CI posting …
onbuyuka Aug 25, 2026
b61a70b
Mark auto-post test as an integration test
onbuyuka Aug 25, 2026
1c5672b
Fix propagation test flake and add skipped-reason diagnostic
onbuyuka Aug 26, 2026
7d9d4bb
Set source code on auto-post test journal template for country locali…
onbuyuka Aug 26, 2026
3e7a814
[Shopify] Address automatic posting review feedback
onbuyuka Aug 27, 2026
73ba397
Merge remote-tracking branch 'origin/main' into bugs/620951-shopify-a…
onbuyuka Aug 28, 2026
7e2fbbc
[Shopify] Address auto-post review feedback and fix build
onbuyuka Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,32 @@ 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; SalesShptHdrNo: Code[20]; SalesInvHdrNo: Code[20]; RetRcpHdrNo: Code[20]; SalesCrMemoHdrNo: Code[20])
Comment thread
onbuyuka marked this conversation as resolved.
Outdated
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;

if PreviewMode then
exit;

CreateDocLinksToBCDocs(SalesHeader, SalesShptHdrNo, SalesInvHdrNo, RetRcpHdrNo, SalesCrMemoHdrNo);

// When the caller suppresses commit it owns the transaction and expects atomicity, so skip the
Comment thread
onbuyuka marked this conversation as resolved.
Outdated
// best-effort automatic payment posting (which would otherwise commit or risk the caller's work).
if CommitIsSuppressed then
Comment thread
onbuyuka marked this conversation as resolved.
Outdated
exit;

ShpfyAutoPostTransactions.AutoPostTransactions(SalesInvHdrNo, SalesCrMemoHdrNo);
Comment thread
onbuyuka marked this conversation as resolved.
Outdated
end;

local procedure CreateDocLinksToBCDocs(var SalesHeader: Record "Sales Header"; SalesShptHdrNo: Code[20]; SalesInvHdrNo: Code[20]; RetRcpHdrNo: Code[20]; SalesCrMemoHdrNo: Code[20])
Comment thread
onbuyuka marked this conversation as resolved.
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.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ 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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟠\ High\ Severity\ —\ AppSource}$

The new automatic-posting feature exposes general-journal setup on the Shopify payment-method mapping page and then creates/posts isolated general-journal batches during invoice/credit-memo posting, but none of the app's assignable Shopify permission sets (built on top of "Shpfy - Objects", which only grants execute permission on the new Shopify objects themselves) add the underlying Gen. Journal Line/Gen. Journal Batch table permissions the feature needs at setup and posting time. A user assigned only the app's Shopify roles cannot configure or exercise this feature without an additional, non-Shopify role, matching the AppSource anti-pattern of shipping a workflow with missing permission coverage.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4

codeunit "Shpfy Auto Post Transactions" = X,
codeunit "Shpfy Background Syncs" = X,
codeunit "Shpfy Balance Today" = X,
codeunit "Shpfy Base64" = X,
Expand Down Expand Up @@ -301,6 +303,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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// ------------------------------------------------------------------------------------------------
// 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.Posting;

/// <summary>
/// Codeunit Shpfy Auto Gen. Jnl.-Post (ID 30422).
/// Builds the general journal line(s) for a single Shopify order/refund payment transaction. It is invoked
/// through Codeunit.Run so that a failure while building is trapped and rolled back without leaving a line
/// behind. While bound, it also pre-confirms the "posting after working date" prompt so the automatic
/// posting stays non-interactive.
/// </summary>
codeunit 30422 "Shpfy Auto Gen. Jnl.-Post"
{
Access = Internal;
EventSubscriberInstance = Manual;
TableNo = "Shpfy Order Transaction";

trigger OnRun()
begin
BuildJournalLines(Rec);
end;

var
PaymentMethodMapping: Record "Shpfy Payment Method Mapping";
PostingDate: Date;

internal procedure SetParameters(NewPaymentMethodMapping: Record "Shpfy Payment Method Mapping"; NewPostingDate: Date)
begin
PaymentMethodMapping := NewPaymentMethodMapping;
PostingDate := NewPostingDate;
end;

local procedure BuildJournalLines(var OrderTransaction: Record "Shpfy Order Transaction")
var
SuggestPayments: Report "Shpfy Suggest Payments";
begin
SuggestPayments.SetJournalParameters(PaymentMethodMapping."Auto-Post Jnl. Template", PaymentMethodMapping."Auto-Post Jnl. Batch", PostingDate);
SuggestPayments.GetOrderTransactions(OrderTransaction);
SuggestPayments.CreateGeneralJournalLines();
end;

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Batch", 'OnBeforeCheckLine', '', false, false)]
local procedure PreconfirmWorkingDateOnBeforeCheckLine(var PostingAfterWorkingDateConfirmed: Boolean)
begin
PostingAfterWorkingDateConfirmed := true;
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// ------------------------------------------------------------------------------------------------
// 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.History;

/// <summary>
/// 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.
/// </summary>
codeunit 30236 "Shpfy Auto Post Transactions"
{
Access = Internal;

internal procedure AutoPostTransactions(SalesInvoiceHeaderNo: Code[20]; SalesCrMemoHeaderNo: Code[20])
Comment thread
onbuyuka marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟠\ High\ Severity\ —\ Events}$

The new automatic-posting pipeline in Shpfy Auto Post Transactions is a posting entry point, but it introduces no OnBefore.../OnAfter... integration events around the core posting flow. That makes eligibility, journal construction, and failure handling a hard wall for extensions, forcing partners to copy or replace the feature instead of subscribing to thin hooks at the operation boundaries.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4

begin
if SalesInvoiceHeaderNo <> '' then
PostOrderTransactions(SalesInvoiceHeaderNo);
if SalesCrMemoHeaderNo <> '' then
PostRefundTransactions(SalesCrMemoHeaderNo);
end;

local procedure PostOrderTransactions(SalesInvoiceHeaderNo: Code[20])
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);
Comment thread
onbuyuka marked this conversation as resolved.
PostTransactions(OrderTransaction, SalesInvoiceHeader."Posting Date");
end;

local procedure PostRefundTransactions(SalesCrMemoHeaderNo: Code[20])
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");
end;

local procedure PostTransactions(var OrderTransaction: Record "Shpfy Order Transaction"; PostingDate: Date)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Interfaces}$

Shpfy Auto Post Transactions hardwires Shpfy Auto Post Eligibility, Shpfy Auto Gen. Jnl.-Post, and Shpfy Auto Post Finalize as concrete Codeunit collaborators. Because the posting and cleanup paths are invoked through concrete codeunits, tests cannot inject doubles to exercise the success, skip, and failure branches of the auto-post flow in isolation. Depend on interfaces and inject the implementations instead of constructing these collaborators directly.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4

var
PaymentMethodMapping: Record "Shpfy Payment Method Mapping";
BoundaryCommitted: Boolean;
begin
OrderTransaction.SetRange(Status, OrderTransaction.Status::Success);
Comment thread
onbuyuka marked this conversation as resolved.
OrderTransaction.SetRange(Used, false);
if not OrderTransaction.FindSet() then
exit;

repeat
Comment thread
onbuyuka marked this conversation as resolved.
if GetAutoPostMapping(OrderTransaction, PaymentMethodMapping) then begin
if not BoundaryCommitted then begin
// The posted document has already been committed by Sales-Post at this point.
// Commit again to establish a rollback boundary before best-effort payment posting,
// so a posting failure only rolls back to here and preserves the posted document and
// its Shopify document links.
Commit();
BoundaryCommitted := true;
end;
PostTransaction(OrderTransaction, PaymentMethodMapping, PostingDate);
end;
until OrderTransaction.Next() = 0;
end;

local procedure GetAutoPostMapping(OrderTransaction: Record "Shpfy Order Transaction"; var PaymentMethodMapping: Record "Shpfy Payment Method Mapping"): Boolean
begin
if not PaymentMethodMapping.Get(OrderTransaction.Shop, OrderTransaction.Gateway, OrderTransaction."Credit Card Company") then
Comment thread
onbuyuka marked this conversation as resolved.
Outdated
exit(false);
if not PaymentMethodMapping."Post Automatically" then
exit(false);
exit((PaymentMethodMapping."Auto-Post Jnl. Template" <> '') and (PaymentMethodMapping."Auto-Post Jnl. Batch" <> ''));
end;

local procedure PostTransaction(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";
AutoGenJnlPost: Codeunit "Shpfy Auto Gen. Jnl.-Post";
TemplateName: Code[10];
BatchName: Code[10];
ErrorText: Text;
begin
TemplateName := PaymentMethodMapping."Auto-Post Jnl. Template";
BatchName := PaymentMethodMapping."Auto-Post Jnl. Batch";

// Guard against lines left behind by a previously interrupted attempt for this transaction.
RemoveJournalLines(TemplateName, BatchName, OrderTransaction."Shopify Transaction Id");

AutoGenJnlPost.SetParameters(PaymentMethodMapping, PostingDate);
BindSubscription(AutoGenJnlPost);

// Build the journal line(s) through Codeunit.Run so a failure while building is trapped and rolled
// back to the boundary commit instead of blocking the document posting.
if not AutoGenJnlPost.Run(OrderTransaction) then begin
Comment thread
onbuyuka marked this conversation as resolved.
Outdated
ErrorText := GetLastErrorText();
UnbindSubscription(AutoGenJnlPost);
LogFailureAndCommit(OrderTransaction, ErrorText);
exit;
end;

GenJournalLine.SetRange("Journal Template Name", TemplateName);
GenJournalLine.SetRange("Journal Batch Name", BatchName);
GenJournalLine.SetRange("Shpfy Transaction Id", OrderTransaction."Shopify Transaction Id");
if GenJournalLine.IsEmpty() then begin
UnbindSubscription(AutoGenJnlPost);
exit;
end;
GenJournalLine.SetRange("Shpfy Transaction Id");
GenJournalLine.FindSet();

// Commit the built line(s) so the batch posting - which commits internally - runs in a valid
// transaction context (matching the platform's own SendToPosting behavior).
Commit();
Comment thread
onbuyuka marked this conversation as resolved.
Outdated

if GenJnlPostBatch.Run(GenJournalLine) then begin
UnbindSubscription(AutoGenJnlPost);
exit;
end;
ErrorText := GetLastErrorText();
UnbindSubscription(AutoGenJnlPost);

// Posting failed after the line(s) were committed: remove them and log the failure without
// blocking or reversing the document posting.
RemoveJournalLines(TemplateName, BatchName, OrderTransaction."Shopify Transaction Id");
LogFailureAndCommit(OrderTransaction, ErrorText);
end;

local procedure RemoveJournalLines(TemplateName: Code[10]; BatchName: Code[10]; ShopifyTransactionId: BigInteger)
var
GenJournalLine: Record "Gen. Journal Line";
begin
GenJournalLine.SetRange("Journal Template Name", TemplateName);
GenJournalLine.SetRange("Journal Batch Name", BatchName);
GenJournalLine.SetRange("Shpfy Transaction Id", ShopifyTransactionId);
if not GenJournalLine.IsEmpty() then
GenJournalLine.DeleteAll(true);
end;

local procedure LogFailureAndCommit(OrderTransaction: Record "Shpfy Order Transaction"; ErrorText: Text)
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(ErrorText, 1, 250), Shop);
// Commit the log (and any cleanup) so a subsequent transaction's rollback cannot discard it.
Commit();
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// ------------------------------------------------------------------------------------------------
// 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;

/// <summary>
/// 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.
/// </summary>
page 30176 "Shpfy Filter Transactions"
{
Caption = 'Filter Postable Transactions';
PageType = StandardDialog;
ApplicationArea = All;
Comment thread
onbuyuka marked this conversation as resolved.

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(0, PaymentMethodMapping) = Action::LookupOK then begin
Comment thread
onbuyuka marked this conversation as resolved.
Outdated
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);
NewEndDate := CreateDateTime(EndDate, 0T);
end;
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ page 30132 "Shpfy Payment Methods Mapping"
ApplicationArea = All;
ToolTip = 'Specifies the corresponding payment method in D365BC.';
}
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.';
}
field(AutoPostJnlTemplate; Rec."Auto-Post Jnl. Template")
Comment thread
onbuyuka marked this conversation as resolved.
{
ApplicationArea = All;
ToolTip = 'Specifies the general journal template to use for automatically posting payment transactions.';
Enabled = Rec."Post Automatically";
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.';
Enabled = Rec."Post Automatically";
ShowMandatory = Rec."Post Automatically";
}
}
}
}
Expand Down
Loading
Loading