diff --git a/src/Apps/W1/Subscription Billing/App/Service Commitments/Tables/SubscriptionLine.Table.al b/src/Apps/W1/Subscription Billing/App/Service Commitments/Tables/SubscriptionLine.Table.al index dd0be51524..30158e57fc 100644 --- a/src/Apps/W1/Subscription Billing/App/Service Commitments/Tables/SubscriptionLine.Table.al +++ b/src/Apps/W1/Subscription Billing/App/Service Commitments/Tables/SubscriptionLine.Table.al @@ -1946,9 +1946,13 @@ table 8059 "Subscription Line" LastDateInLastMonth := CalcDate(PeriodFormula, FromDate); LastDateInLastMonth := CalcDate('', LastDateInLastMonth); NextToDate := LastDateInLastMonth - DistanceToEndOfMonth - 1; + if NextToDate < FromDate then + NextToDate := CalcDate(PeriodFormula, FromDate) - 1; end; end; end; + if NextToDate < FromDate then + NextToDate := FromDate; end; local procedure GetBillingReferenceDate() BillingReferenceDate: Date diff --git a/src/Apps/W1/Subscription Billing/Test/Billing/RecurringBillingDocsTest.Codeunit.al b/src/Apps/W1/Subscription Billing/Test/Billing/RecurringBillingDocsTest.Codeunit.al index 969753674b..710849ca10 100644 --- a/src/Apps/W1/Subscription Billing/Test/Billing/RecurringBillingDocsTest.Codeunit.al +++ b/src/Apps/W1/Subscription Billing/Test/Billing/RecurringBillingDocsTest.Codeunit.al @@ -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; @@ -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 @@ -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