Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -1946,9 +1946,13 @@ table 8059 "Subscription Line"
LastDateInLastMonth := CalcDate(PeriodFormula, FromDate);
LastDateInLastMonth := CalcDate('<CM>', LastDateInLastMonth);
NextToDate := LastDateInLastMonth - DistanceToEndOfMonth - 1;
if NextToDate < FromDate then
NextToDate := CalcDate(PeriodFormula, FromDate) - 1;
end;
end;
end;
if NextToDate < FromDate then
NextToDate := FromDate;
end;
Comment on lines 1946 to 1956
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

There are now two separate clamps (if NextToDate < FromDate) with slightly different fallback logic, which makes the effective behavior harder to reason about (especially for boundary formulas like <0D> where CalcDate(..) - 1 still ends up < FromDate and is then overridden). Consider consolidating to a single, clearly documented normalization step at the end (or using a single helper/local variable like CandidateToDate) so the intent is explicit: compute the best-effort NextToDate, then normalize to ensure it is >= FromDate.

Copilot uses AI. Check for mistakes.

local procedure GetBillingReferenceDate() BillingReferenceDate: Date
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ codeunit 139687 "Recurring Billing Docs Test"
LibraryUtility: Codeunit "Library - Utility";
LibraryVariableStorage: Codeunit "Library - Variable Storage";
IsInitialized: Boolean;
NextToDateBeforeFromDateErr: Label 'CalculateNextToDate returned %1 which is before FromDate %2. This would cause billing to get stuck.', Locked = true;
NoContractLinesFoundErr: Label 'No contract lines were found that can be billed with the specified parameters.', Locked = true;
StrMenuHandlerStep: Integer;

Expand Down Expand Up @@ -2452,6 +2453,90 @@ codeunit 139687 "Recurring Billing Docs Test"
CatalogItem.TestField("Subscription Option", "Item Service Commitment Type"::"Service Commitment Item");
end;

[Test]
procedure CalculateNextToDateAlignEndOfMonthDoesNotReturnDateBeforeFromDate()
var
SubscriptionLine: Record "Subscription Line";
PeriodFormula: DateFormula;
FromDate: Date;
NextToDate: Date;
begin
// [FEATURE] [AI test]
// [SCENARIO 623011] CalculateNextToDate with "Align to End of Month" does not return date before FromDate
Initialize();

// [GIVEN] Subscription Line "SL" with Period Calculation = "Align to End of Month" and start date Jan 29
MockSubscriptionLine(SubscriptionLine);
SubscriptionLine.Validate("Period Calculation", SubscriptionLine."Period Calculation"::"Align to End of Month");
SubscriptionLine.Validate("Subscription Line Start Date", DMY2Date(29, 1, 2025));
SubscriptionLine.Modify(true);

// [WHEN] CalculateNextToDate is called with period formula <1D> from Feb 27
Evaluate(PeriodFormula, '<1D>');
FromDate := DMY2Date(27, 2, 2025);
NextToDate := SubscriptionLine.CalculateNextToDate(PeriodFormula, FromDate);

// [THEN] NextToDate is not before FromDate
Assert.IsTrue(NextToDate >= FromDate,
StrSubstNo(NextToDateBeforeFromDateErr, NextToDate, FromDate));
end;

[Test]
procedure CalculateNextToDateZeroDayFormulaReturnsFromDate()
var
SubscriptionLine: Record "Subscription Line";
PeriodFormula: DateFormula;
FromDate: Date;
NextToDate: Date;
begin
// [FEATURE] [AI test]
// [SCENARIO 623011] CalculateNextToDate with <0D> formula returns exactly FromDate (boundary case)
Initialize();

// [GIVEN] Subscription Line "SL" with Period Calculation = "Align to End of Month" and start date Jan 31
MockSubscriptionLine(SubscriptionLine);
SubscriptionLine.Validate("Period Calculation", SubscriptionLine."Period Calculation"::"Align to End of Month");
SubscriptionLine.Validate("Subscription Line Start Date", DMY2Date(31, 1, 2025));
SubscriptionLine.Modify(true);

// [WHEN] CalculateNextToDate is called with period formula <0D> from Feb 28
Evaluate(PeriodFormula, '<0D>');
FromDate := DMY2Date(28, 2, 2025);
NextToDate := SubscriptionLine.CalculateNextToDate(PeriodFormula, FromDate);

// [THEN] NextToDate is not before FromDate (should be exactly FromDate or later)
Assert.IsTrue(NextToDate >= FromDate,
StrSubstNo(NextToDateBeforeFromDateErr, NextToDate, FromDate));
end;

[Test]
procedure CalculateNextToDateMonthFormulaLeapYearDoesNotReturnDateBeforeFromDate()
var
SubscriptionLine: Record "Subscription Line";
PeriodFormula: DateFormula;
FromDate: Date;
NextToDate: Date;
begin
// [FEATURE] [AI test]
// [SCENARIO 623011] CalculateNextToDate with monthly formula and leap year Feb 29 to Mar transition does not return date before FromDate
Initialize();

// [GIVEN] Subscription Line "SL" with Period Calculation = "Align to End of Month" and start date Jan 30 (leap year 2024)
MockSubscriptionLine(SubscriptionLine);
SubscriptionLine.Validate("Period Calculation", SubscriptionLine."Period Calculation"::"Align to End of Month");
SubscriptionLine.Validate("Subscription Line Start Date", DMY2Date(30, 1, 2024));
SubscriptionLine.Modify(true);

// [WHEN] CalculateNextToDate is called with period formula <1M> from Feb 29 (leap year)
Evaluate(PeriodFormula, '<1M>');
FromDate := DMY2Date(29, 2, 2024);
NextToDate := SubscriptionLine.CalculateNextToDate(PeriodFormula, FromDate);

// [THEN] NextToDate is not before FromDate
Assert.IsTrue(NextToDate >= FromDate,
StrSubstNo(NextToDateBeforeFromDateErr, NextToDate, FromDate));
end;

#endregion Tests

#region Procedures
Expand Down Expand Up @@ -2925,6 +3010,17 @@ codeunit 139687 "Recurring Billing Docs Test"
end;
end;

local procedure MockSubscriptionLine(var SubscriptionLine: Record "Subscription Line")
var
ServiceCommitmentPackage: Record "Subscription Package";
begin
ContractTestLibrary.CreateServiceCommitmentPackage(ServiceCommitmentPackage);
SubscriptionLine.Init();
SubscriptionLine.Validate("Invoicing via", SubscriptionLine."Invoicing via"::Contract);
SubscriptionLine.Validate("Subscription Package Code", ServiceCommitmentPackage.Code);
SubscriptionLine.Insert(true);
end;

#endregion Procedures

#region Handlers
Expand Down
Loading