Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -483,6 +483,7 @@ codeunit 8062 "Billing Proposal"
local procedure CalculateBillingPeriod(ServiceCommitment: Record "Subscription Line"; BillingDate: Date; BillToDate: Date; var BillingPeriodStart: Date; var BillingPeriodEnd: Date)
var
UsageDataBilling: Record "Usage Data Billing";
PreviousBillingPeriodEnd: Date;
begin
BillingPeriodEnd := 0D;
BillingPeriodStart := ServiceCommitment."Next Billing Date";
Expand All @@ -505,10 +506,14 @@ codeunit 8062 "Billing Proposal"
end;

BillingPeriodEnd := CalculateNextBillingToDateForServiceCommitment(ServiceCommitment, BillingPeriodStart);
PreviousBillingPeriodEnd := 0D;
while (BillingPeriodEnd < BillingDate) and
((BillingPeriodEnd < ServiceCommitment."Subscription Line End Date") or (ServiceCommitment."Subscription Line End Date" = 0D))
do

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\ —\ Agent} \quad \color{gray}{\texttt{\small Iteration\ 1}}$

The new loop-progress guard (BillingPeriodEnd > PreviousBillingPeriodEnd) correctly prevents an infinite loop, but when it fires it exits silently: the var output parameter BillingPeriodEnd is left holding the non-advancing value returned by CalculateNextBillingToDateForServiceCommitment, and the caller receives no signal distinguishing a normal exit (billing date or end-date boundary reached) from an abnormal one (the inner function failed to advance the period).

In subscription billing, a silently incorrect BillingPeriodEnd can produce wrong invoice amounts or missed billing lines.

Recommendation:

  • after the loop, detect the abnormal case — for example by checking whether the guard condition would have allowed another iteration but didn't — and raise an Error or emit a telemetry signal so the failure surface is observable.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why

((BillingPeriodEnd < ServiceCommitment."Subscription Line End Date") or (ServiceCommitment."Subscription Line End Date" = 0D)) and
(BillingPeriodEnd > PreviousBillingPeriodEnd)
do begin
PreviousBillingPeriodEnd := BillingPeriodEnd;
BillingPeriodEnd := CalculateNextBillingToDateForServiceCommitment(ServiceCommitment, BillingPeriodEnd + 1);
end;
end;

procedure CalculateNextBillingToDateForServiceCommitment(ServiceCommitment: Record "Subscription Line"; BillingFromDate: Date) NextBillingToDate: Date
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ codeunit 139688 "Recurring Billing Test"
PostedDocumentNo: Code[20];
StrMenuHandlerStep: Integer;
BillingProposalNotCreatedErr: Label 'Billing proposal not created.', Locked = true;
BillingToCappedErr: Label 'Billing to should be capped at the harmonized Next Billing To date.', Locked = true;
ExtendedTextValueErr: Label 'Sales line with extended text description should be created.', Locked = true;
ExtendedTextPurchValueErr: Label 'Purchase line with extended text description should be created.', Locked = true;
RecurringBillingPage: TestPage "Recurring Billing";
Expand Down Expand Up @@ -1571,6 +1572,49 @@ codeunit 139688 "Recurring Billing Test"
Assert.AreEqual(ExpectedNextBillingDate, ServiceCommitment."Next Billing Date", 'Next Billing Date should be the day after the last Billing To date.');
end;

[Test]
procedure CreateBillingProposalForHarmonizedContractWithCappedNextBillingToDoesNotHang()
var
ContractType: Record "Subscription Contract Type";
BillingDate: Date;
CappedNextBillingTo: Date;
begin
// [SCENARIO 637042] Creating a Billing Proposal for a harmonized billing contract must not hang when the contract "Next Billing To" caps the calculated billing period below the Billing Date
Initialize();

// [GIVEN] An LCY Customer Subscription Contract with a monthly Subscription Line
CreateCustomerContract('<1M>', '<12M>');
FindFirstServiceCommitment();

// [GIVEN] The contract uses a harmonized billing contract type
ContractTestLibrary.CreateContractType(ContractType);
ContractType.HarmonizedBillingCustContracts := true;
ContractType.Modify(false);

// [GIVEN] The harmonized "Next Billing To" is frozen at the first billing period end, which is earlier than the Billing Date
CappedNextBillingTo := ServiceCommitment.CalculateNextToDate(ServiceCommitment."Billing Rhythm", ServiceCommitment."Next Billing Date");
BillingDate := CalcDate('<+6M>', ServiceCommitment."Next Billing Date");
CustomerContract.Get(CustomerContract."No.");
CustomerContract."Contract Type" := ContractType.Code;
CustomerContract."Next Billing To" := CappedNextBillingTo;
CustomerContract.Modify(false);

// [GIVEN] A Billing Template filtered on the harmonized contract
CustomerContract.SetRecFilter();
ContractTestLibrary.CreateRecurringBillingTemplate(BillingTemplate, '', '', CustomerContract.GetView(), Enum::"Service Partner"::Customer);

// [WHEN] Create Billing Proposal up to a Billing Date that lies beyond the frozen "Next Billing To"
// [THEN] The process completes without entering an infinite loop in CalculateBillingPeriod (before the fix it would spin until a SQL timeout)
ContractTestLibrary.CreateBillingProposal(BillingTemplate, Enum::"Service Partner"::Customer, BillingDate, 0D);

// [THEN] A Billing Line is created and its "Billing to" is capped at the harmonized "Next Billing To" date
BillingLine.Reset();
BillingLine.SetRange("Subscription Contract No.", CustomerContract."No.");
Assert.RecordIsNotEmpty(BillingLine);
BillingLine.FindLast();
Assert.AreEqual(CappedNextBillingTo, BillingLine."Billing to", BillingToCappedErr);
end;

#endregion Tests

#region Procedures
Expand Down
Loading