{"record":{"id":"96499ad4949dbb47","repo":"juspay/hyperswitch","slug":"grace-2-some","errorCode":null,"errorMessage":"grace 2 => Some","messagePattern":"grace 2 => Some","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/router/src/workflows/revenue_recovery.rs","lineNumber":2096,"sourceCode":"                    .expect(\"grace > 1 => Some\");\n                let last = (before + time::Duration::days(i64::from(grace) + 1)).date();\n                assert!(\n                    dt.date() > before.date() && dt.date() <= last,\n                    \"date {} out of window\",\n                    dt.date()\n                );\n                assert!(dt.hour() < 24);\n            }\n        }\n    }\n\n    #[test]\n    fn window_starts_next_day() {\n        // Failure day is excluded: the earliest candidate is tomorrow. grace COUNTS today, so grace 2\n        // = today + 1 future day (tomorrow) — assert the pick is that next day, not the failure day.\n        let before = common_utils::date_time::now().assume_utc();\n        let dt = compute_mathmodel_retry_time(&sample(), 3, 2, DEFAULT_RETRY_HOUR)\n            .expect(\"grace 2 => Some\");\n        assert!(\n            dt.date() > before.date(),\n            \"expected next day, got {} (today {})\",\n            dt.date(),\n            before.date()\n        );\n        assert!(dt.date() <= (before + time::Duration::days(2)).date());\n    }\n\n    #[test]\n    fn grace_zero_and_one_return_none() {\n        // grace COUNTS today; grace 0 = no grace, grace 1 = today only -> no future day -> None (v1).\n        assert!(compute_mathmodel_retry_time(&sample(), 3, 0, DEFAULT_RETRY_HOUR).is_none());\n        assert!(compute_mathmodel_retry_time(&sample(), 3, 1, DEFAULT_RETRY_HOUR).is_none());\n    }\n\n    #[test]\n    fn zero_budget_returns_none() {","sourceCodeStart":2078,"sourceCodeEnd":2114,"githubUrl":"https://github.com/juspay/hyperswitch/blob/afd92843521ced2c6856b98ae4ea332e75102d88/crates/router/src/workflows/revenue_recovery.rs#L2078-L2114","documentation":"Test-time expect() in window_starts_next_day (crates/router/src/workflows/revenue_recovery.rs:2096): compute_mathmodel_retry_time(&sample(), 3, grace=2, DEFAULT_RETRY_HOUR) returned None where the test requires Some for grace 2. The semantic contract is that the failure day is excluded and grace 2 yields exactly tomorrow, so a None means the next-day window computation failed to produce a candidate.","triggerScenarios":"Calling compute_mathmodel_retry_time with grace=2 on the sample StatsDocument and getting None — the day-window [tomorrow] came back empty because the function excluded the single eligible future day (off-by-one in inclusive/exclusive grace counting or hour-slot filtering dropped the only day).","commonSituations":"Refactors that changed whether grace counts today (inclusive) versus starts tomorrow; DEFAULT_RETRY_HOUR hour-slot filtering removing the only candidate day/hour; timezone/date arithmetic changes shifting the 'before' date boundary (midnight ticks).","solutions":["Check the day-window construction for grace=2: it must include exactly the next calendar day; fix any off-by-one that turns the window empty.","Ensure hour-slot selection always accepts DEFAULT_RETRY_HOUR when only one day is in the window (no filtering that can drop the sole candidate).","Test around midnight locally — if flaky only at boundaries, normalize the 'today' anchor date inside the function before windowing.","Keep the contract documented: grace counts today, so Some must be guaranteed for grace >= 2; fix the function, not the test."],"exampleFix":"// before\nlet dt = compute_mathmodel_retry_time(&sample(), 3, 2, DEFAULT_RETRY_HOUR)\n    .expect(\"grace 2 => Some\");\n// after (in the function)\nlet start = today + 1; // failure day excluded\nlet end = today + grace; // inclusive: grace 2 => [tomorrow]\nassert!(start <= end, \"window empty for grace {grace}\"); // fail loudly in dev, not None in tests\n// and select from [start..=end] falling back to DEFAULT_RETRY_HOUR on the chosen day","handlingStrategy":"fallback","validationCode":"// assert the window is non-empty before selection:\nlet start = today + 1;\nlet end = today + grace; // inclusive\nassert!(start <= end, \"empty retry window for grace {grace}\");","typeGuard":null,"tryCatchPattern":"let dt = compute_mathmodel_retry_time(&sample(), 3, 2, DEFAULT_RETRY_HOUR)\n    .unwrap_or_else(|| tomorrow_at(DEFAULT_RETRY_HOUR));","preventionTips":["Keep the documented contract: grace counts today, failure day excluded, Some guaranteed for grace >= 2","Add midnight-boundary tests so date arithmetic can't silently empty the window","Don't filter the sole candidate day/hour without a fallback","Fix the function, never relax this assertion"],"tags":["test-failure","scheduling","date-arithmetic","off-by-one"],"backgroundTag":"empty-result-set","analyzedSha":"afd92843521ced2c6856b98ae4ea332e75102d88","analyzedAt":"2026-09-18T01:18:02.947Z","contentChangedAt":"2026-09-18T01:18:02.947Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}