{"record":{"id":"faa076f77d75c53c","repo":"juspay/hyperswitch","slug":"grace-1-some","errorCode":null,"errorMessage":"grace > 1 => Some","messagePattern":"grace > 1 => Some","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/router/src/workflows/revenue_recovery.rs","lineNumber":2078,"sourceCode":"            assert!(pick_hour(&doc.hod, DEFAULT_RETRY_HOUR) < 24);\n        }\n        assert_eq!(\n            pick_hour(&[SlotCounter::default(); 24], DEFAULT_RETRY_HOUR),\n            DEFAULT_RETRY_HOUR\n        );\n    }\n\n    #[test]\n    fn result_is_always_in_window() {\n        // Invariant for EVERY random outcome: result date ∈ [tomorrow, today+grace], hour ∈ 0..23,\n        // never panics — for both rich and empty stats. (Window starts on the NEXT day; the bounds\n        // carry a 1-day slack so a midnight tick between captures can't flake it.)\n        let grace: u32 = 14;\n        for stats in [sample(), StatsDocument::default()] {\n            for _ in 0..200 {\n                let before = common_utils::date_time::now().assume_utc();\n                let dt = compute_mathmodel_retry_time(&stats, 3, grace, DEFAULT_RETRY_HOUR)\n                    .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\");","sourceCodeStart":2060,"sourceCodeEnd":2096,"githubUrl":"https://github.com/juspay/hyperswitch/blob/afd92843521ced2c6856b98ae4ea332e75102d88/crates/router/src/workflows/revenue_recovery.rs#L2060-L2096","documentation":"Test-time expect() in crates/router/src/workflows/revenue_recovery.rs:2078: compute_mathmodel_retry_time(&stats, 3, grace=14, DEFAULT_RETRY_HOUR) returned None (or errored) for a case the test asserts must always produce a value when grace > 1. The function picks the next retry datetime within a grace window derived from StatsDocument slot weights; a None means the scheduler failed to find any candidate retry slot despite a non-trivial grace period.","triggerScenarios":"Calling compute_mathmodel_retry_time with grace > 1 on either a populated sample() StatsDocument or the all-defaults StatsDocument::default() and receiving None — e.g. the candidate-window computation excludes all days/hours so no retry slot is produced, an internal regression in the mathmodel picker.","commonSituations":"A code change to compute_mathmodel_retry_time (window arithmetic, slot filtering, DEFAULT_RETRY_HOUR handling) that makes it return None for zero/sparse stats; changing DEFAULT_RETRY_HOUR to an hour that gets filtered out; off-by-one in day-grace arithmetic so the window is empty.","solutions":["Run the test and inspect why the picker returned None: add temporary logging inside compute_mathmodel_retry_time for the computed window bounds.","Check the grace-window day arithmetic: with grace=14 the candidate window must always contain at least one future day (before+1 .. before+grace+1).","Verify StatsDocument::default() (all-zero weights) falls back to a uniform/valid slot selection rather than early-returning None.","Confirm DEFAULT_RETRY_HOUR is a valid hour (0-23) that survives slot filtering; if the hour is filtered, the fallback must still yield a datetime.","Fix the regression in the window/slot logic rather than relaxing the test assertion."],"exampleFix":"// before\nlet dt = compute_mathmodel_retry_time(&stats, 3, grace, DEFAULT_RETRY_HOUR)\n    .expect(\"grace > 1 => Some\");\n// after (in the function, guarantee the invariant)\ncompute_uniform_slot(&stats, max_retries, grace, retry_hour)\n    .or_else(|| fallback_next_day(retry_hour, grace)) // never None when grace >= 2\n// before(production-shape fix): return None on empty window\n// after: clamp window end to at least start + 1 day so Some is guaranteed for grace > 1","handlingStrategy":"fallback","validationCode":"// caller-side guard before relying on the result:\nlet dt = compute_mathmodel_retry_time(&stats, retries, grace, retry_hour)\n    .unwrap_or_else(|| default_retry_datetime(grace, retry_hour));","typeGuard":null,"tryCatchPattern":"match compute_mathmodel_retry_time(&stats, 3, grace, DEFAULT_RETRY_HOUR) {\n    Some(dt) => dt,\n    None => fallback_next_day_retry(DEFAULT_RETRY_HOUR, grace),\n}","preventionTips":["Guarantee non-None for grace > 1 with a uniform/next-day fallback inside the function","Cover StatsDocument::default() (zero weights) in unit tests whenever the picker changes","Validate DEFAULT_RETRY_HOUR stays within 0..24 in config","Add debug logging of computed window bounds when returning None"],"tags":["test-failure","scheduling","revenue-recovery","invariant-violation"],"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"}