[Bug][Subscription Billing] Contract Renewal Quote total ignores the Billing Base Period and shows the base-period amount instead of the renewal-term amount - #10445
Conversation
## Why The subscription totals block on a printed Contract Renewal Quote showed the amount per Billing Base Period instead of the amount for the renewal term the customer is actually asked to sign. `CalcVATAmountLines` -> `CreateTempSalesServiceCommitmentBuffForSalesServiceCommitment` converted `Amount` from the Billing Base Period to the Billing Rhythm only in the regular branch. The contract-renewal branch multiplied by the renewal term ratio alone, which expresses how many billing rhythms fit into the renewal term, not the base-period-to-rhythm ratio. With Billing Base Period 1M, Billing Rhythm 12M and Renewal Term 12M that ratio is exactly 1, so a line priced at 100 per month printed 100 instead of 1,200. The defect stayed invisible in the common 1M/1M case, where both factors are 1. ## Summary - **Moved** the two `FindDateFormulaTypeForComparison` lookups above the branch so the regular and the contract-renewal path share one conversion, and applied `/ BasePeriodCount * RhythmPeriodCount` in the contract-renewal branch as well. - **Changed** `BasePeriodCount` and `RhythmPeriodCount` from var parameters threaded in from `CalcVATAmountLines` into locals of the procedure, re-initialised per line. As var parameters they were initialised once per document, and `FindDateFormulaType` leaves `PeriodCount` untouched for empty, complex and current-period formulas, so one line's period count leaked into the next. - **Added** three tests: the reported 1M/12M scenario, the empty Billing Base Period fallback, and a complex Billing Rhythm that has no period count. - **Corrected** the expectation in `TestContractRenewalPeriodCalculation`, which was asserting the defect because its base data uses Billing Base Period 1Y with Billing Rhythm 1M. It now states the conversion factor as a literal guarded by `TestField` instead of deriving it through Date Formula Management, so the assertion can no longer follow the implementation into a wrong conversion. Fixes microsoft#10356 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Agentic PR Review - Round 1Recommendation: AcceptWhat this PR doesThis PR fixes the Contract Renewal Quote subscription totals when the Billing Base Period and Billing Rhythm differ. The linked issue describes a clear bug: a line priced at 100 per month, billed yearly, printed 100 instead of the 1,200 renewal-term total. The diff targets the right place: CalcVATAmountLines builds the temporary totals used by the report, and the contract-renewal branch now applies the same base-period-to-rhythm conversion as the regular branch before applying the renewal-term ratio. Moving BasePeriodCount and RhythmPeriodCount into the helper also fixes the prior cross-line leak when a later line has an empty or complex formula. I did not find a BaseApp event dependency in this PR. SuggestionsNone. Risk assessment and necessityRisk: This affects computed totals for Subscription Billing sales documents, including VAT amount-line buffers used by reports/statistics. The change is narrow and keeps the existing non-renewal arithmetic, but it can change totals for renewal quotes and for regular multi-line quotes that previously reused period counts from an earlier line. The added tests cover the reported 1M/12M renewal case, empty base period fallback, complex rhythm fallback, and the existing non-renewal branch. Necessity: The issue evidence is strong because the printed quote can show the customer the wrong renewal amount. The fix is in the calculation path that produces that total, has no schema or public signature change, and is scoped to the documented defect.
|
What & why
The subscription totals block on a printed Contract Renewal Quote showed the amount per Billing Base Period instead of the amount for the renewal term the customer is actually being asked to sign. With Billing Base Period
1M, Billing Rhythm12Mand a Renewal Term of12M, a Subscription Line priced at 100 per month printed a total of 100 instead of 1,200."Sales Subscription Line".CalcVATAmountLinesdelegates to the local procedureCreateTempSalesServiceCommitmentBuffForSalesServiceCommitment, which convertedAmountfrom the Billing Base Period to the Billing Rhythm only in the regular branch. The contract-renewal branch multiplied byCalculateRenewalTermRatioByBillingRhythmalone — that ratio expresses how many Billing Rhythms fit into the renewal term, not how the base period relates to the rhythm. When the two happen to coincide the ratio is exactly 1, which is why the defect stayed invisible in the common1M/1Mcase where both factors are 1.This change moves the two
FindDateFormulaTypeForComparisonlookups above the branch so both paths share one conversion, and applies/ BasePeriodCount * RhythmPeriodCountin the contract-renewal branch as well:It also turns
BasePeriodCountandRhythmPeriodCountfromvarparameters threaded in fromCalcVATAmountLinesinto locals of the procedure, re-initialised to 1 per line. Asvarparameters they were initialised once per document, andFindDateFormulaTypeleavesPeriodCountuntouched for empty, complex and current-period formulas — so one line's period count leaked into the next.Linked work
Fixes #10356
How I validated this
What I tested and the outcome (required — be specific: scenarios, commands, screenshots for UI changes)
Container
sb10356. The Subscription Billing App, Test and Demo Data apps were built withalc.exeand published to that container, where the tests below were executed and the renewal quote total was also checked manually.Build — clean, with CodeCop, UICop and PerTenantExtensionCop plus
base.ruleset.json. No analyzer warnings from the changed files.TDD (red first) — with the fix reverted, the new test
CheckVatCalculationForContractRenewalWithBillingBasePeriodDifferentFromBillingRhythmfailed with:which is exactly the number reported in the issue.
Green — codeunit
139692 "Contract Renewal Test", run withal_run_testsagainstsb10356. The four relevant tests: 5 passed, 0 failed.CheckVatCalculationForContractRenewalWithBillingBasePeriodDifferentFromBillingRhythm(new) — the reported1Mbase period /12Mrhythm /12Mrenewal term scenario.CheckVatCalculationForContractRenewalWithoutBillingBasePeriod(new) — empty Billing Base Period falls back to a count of 1, the same way the regular branch does.CheckVatCalculationForContractRenewalWithComplexBillingRhythm(new) — a<1M+1D>rhythm has no period count, so the amount is taken as it is rather than multiplied by zero.CheckVatCalculationForContractRenewalServiceCommitmentRhythmInReports(existing) — its oracle in the helperTestContractRenewalPeriodCalculationwas asserting the defect, because the base data uses Billing Base Period1Ywith Billing Rhythm1M. The expectation now states the conversion factor as a literal guarded byTestField("Billing Base Period", ...)/TestField("Billing Rhythm", ...)instead of deriving it through Date Formula Management, so the assertion can no longer follow the implementation into a wrong conversion.Codeunit 139915 "Sales Service Commitment Test" — covers the non-renewal branch of the same procedure, which this change also touches: 66 passed, 0 failed.
Mutation testing — executed, not reasoned: for each mutant the source was edited, rebuilt, republished and the tests re-run, then reverted. 7 mutants, all killed.
* RhythmPeriodCount...WithBillingBasePeriodDifferentFromBillingRhythm/ BasePeriodCount...ServiceCommitmentRhythmInReports(multi-term)* ContractRenewalPriceCalculationRatio...ServiceCommitmentRhythmInReports(multi-term)BasePeriodCount := 1...WithoutBillingBasePeriodRhythmPeriodCount := 1...WithComplexBillingRhythmDateFormulaType::Year: PeriodCountForComparison * 12fromDateFormulaManagement.FindDateFormulaTypeForComparisonTestContractRenewalPeriodCalculationExpected:<15.11> Actual:<181.28>Mutants 5 and 6 survived the first round;
...WithoutBillingBasePeriodand...WithComplexBillingRhythmwere written specifically to kill them. Mutant 7 reproduces the original defect through the date-formula helper — the old oracle stayed green on it, which is what demonstrates that the oracle correction has teeth.This change adds no new
Label,CaptionorToolTip, so it introduces no translatable strings and there is nothing to add to the XLIFF files.Risk & compatibility
"Sales Service Commitment Buff."when the document is printed or statistics are opened, so existing open Contract Renewal Quotes will display the corrected total on their next print without any upgrade step. No table schema change, no permission, telemetry or upgrade impact.FindDateFormulaTypeForComparisonlookups moved above the branch and the two period counts became locals. The fixed leak ofBasePeriodCount/RhythmPeriodCountbetween lines can also change a regular multi-line quote whose lines use different rhythms — previously the second line could inherit the first line's counts when its own formula is empty, complex or current-period.local procedure CreateTempSalesServiceCommitmentBuffForSalesServiceCommitment; the publicCalcVATAmountLinessignature is untouched, so nothing external can be broken by it.Subscriptions*detail rows printSalesSubscriptionLine.Priceinto "Unit Price" with no period indication. Fixing that means adding a column to three report layout.docxfiles, and the issue's "Expected behavior" section covers only the totals block. Left for a follow-up.