From 2c851725a5f6366373c9a50f29905e9aebc6dbeb Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Sat, 16 May 2026 10:28:32 +0330 Subject: [PATCH] Fixed timing issue with UTC --- src/helpers/iran-datetime.spec.ts | 26 ++++++ src/helpers/iran-datetime.ts | 82 +++++++++++++++++++ .../request-management.service.ts | 27 ++---- 3 files changed, 114 insertions(+), 21 deletions(-) create mode 100644 src/helpers/iran-datetime.spec.ts create mode 100644 src/helpers/iran-datetime.ts diff --git a/src/helpers/iran-datetime.spec.ts b/src/helpers/iran-datetime.spec.ts new file mode 100644 index 0000000..7872d3d --- /dev/null +++ b/src/helpers/iran-datetime.spec.ts @@ -0,0 +1,26 @@ +import { parseIranLocalDateTime } from "./iran-datetime"; + +describe("parseIranLocalDateTime", () => { + it("parses YYYY-MM-DD + HH:MM as Iran local (not server local)", () => { + const instant = parseIranLocalDateTime("2025-05-16", "18:28"); + expect(instant).not.toBeNull(); + // 18:28 Iran = 14:58 UTC + expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z"); + }); + + it("parses naive ISO datetime as Iran local", () => { + const instant = parseIranLocalDateTime("2025-05-16T18:28"); + expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z"); + }); + + it("respects explicit UTC offset in string", () => { + const instant = parseIranLocalDateTime("2025-05-16T14:58:00.000Z"); + expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z"); + }); + + it("combines Date calendar day in Iran with accidentTime", () => { + const dateOnly = new Date("2025-05-16T00:00:00.000Z"); + const instant = parseIranLocalDateTime(dateOnly, "18:28"); + expect(instant!.toISOString()).toBe("2025-05-16T14:58:00.000Z"); + }); +}); diff --git a/src/helpers/iran-datetime.ts b/src/helpers/iran-datetime.ts new file mode 100644 index 0000000..e603910 --- /dev/null +++ b/src/helpers/iran-datetime.ts @@ -0,0 +1,82 @@ +/** Iran standard time (Asia/Tehran, UTC+3:30; no DST since 2022). */ +export const IRAN_TIMEZONE = "Asia/Tehran"; + +const IRAN_UTC_OFFSET = "+03:30"; + +const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/; +const TIME_HM_RE = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/; +const NAIVE_ISO_DATETIME_RE = + /^(\d{4}-\d{2}-\d{2})[T\s](\d{1,2}:\d{2}(?::\d{2})?)$/; + +/** + * Gregorian calendar date (`YYYY-MM-DD`) for an instant in Iran. + */ +export function gregorianDateInIran(d: Date): string { + return new Intl.DateTimeFormat("en-CA", { + timeZone: IRAN_TIMEZONE, + year: "numeric", + month: "2-digit", + day: "2-digit", + }).format(d); +} + +function normalizeTimeToHms(time: string): string | null { + const m = String(time).trim().match(TIME_HM_RE); + if (!m) return null; + const hh = m[1].padStart(2, "0"); + const mm = m[2]; + const ss = m[3] ?? "00"; + return `${hh}:${mm}:${ss}`; +} + +function parseIranLocalIso(isoDate: string, timeHms: string): Date | null { + const d = new Date(`${isoDate}T${timeHms}${IRAN_UTC_OFFSET}`); + return Number.isNaN(d.getTime()) ? null : d; +} + +/** + * Combines a calendar date and optional `HH:MM` / `HH:MM:SS` as Iran local + * time and returns the corresponding UTC instant. + */ +export function parseIranLocalDateTime( + date: Date | string, + time?: string, +): Date | null { + if (date instanceof Date) { + if (Number.isNaN(date.getTime())) return null; + const cleanTime = (time ?? "").trim(); + if (!cleanTime) return date; + const timeHms = normalizeTimeToHms(cleanTime); + if (!timeHms) return null; + return parseIranLocalIso(gregorianDateInIran(date), timeHms); + } + + const raw = String(date).trim(); + if (!raw) return null; + + if (/[zZ]|[+-]\d{2}:?\d{2}$/.test(raw)) { + const d = new Date(raw); + return Number.isNaN(d.getTime()) ? null : d; + } + + const naive = raw.match(NAIVE_ISO_DATETIME_RE); + if (naive) { + const timeHms = normalizeTimeToHms(naive[2]); + if (!timeHms) return null; + return parseIranLocalIso(naive[1], timeHms); + } + + if (DATE_ONLY_RE.test(raw)) { + const cleanTime = (time ?? "").trim(); + if (!cleanTime) { + const d = new Date(`${raw}T00:00:00${IRAN_UTC_OFFSET}`); + return Number.isNaN(d.getTime()) ? null : d; + } + const timeHms = normalizeTimeToHms(cleanTime); + if (!timeHms) return null; + return parseIranLocalIso(raw, timeHms); + } + + const d = new Date(raw); + return Number.isNaN(d.getTime()) ? null : d; +} diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index 7d54661..7f6c873 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -41,6 +41,7 @@ import { ReqBlameStatus } from "src/Types&Enums/blame-request-management/status. import { StepsEnum } from "src/Types&Enums/blame-request-management/steps.enum"; import { ExpertDbService } from "src/users/entities/db-service/expert.db.service"; import { UserDbService } from "src/users/entities/db-service/user.db.service"; +import { parseIranLocalDateTime } from "src/helpers/iran-datetime"; import { AutoCloseRequestService } from "src/utils/cron/cron.service"; import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service"; import { @@ -395,32 +396,16 @@ export class RequestManagementService { /** * Best-effort parser that combines a date input with an optional `HH:MM` - * time. Accepts a `Date`, an ISO datetime string, or an ISO date-only - * string so DTO payloads (which currently send `"YYYY-MM-DD"` + `"HH:MM"`) - * can be passed through directly. Returns `null` when the result is not a - * finite instant. + * time in Iran (Asia/Tehran, UTC+3:30). Accepts a `Date`, an ISO datetime + * string, or an ISO date-only string so DTO payloads (which send + * `"YYYY-MM-DD"` + `"HH:MM"` in local Iran time) can be passed through + * directly. Returns `null` when the result is not a finite instant. */ private parseAccidentInstant( date: Date | string, time?: string, ): Date | null { - if (date instanceof Date) { - return Number.isNaN(date.getTime()) ? null : date; - } - - const raw = String(date).trim(); - if (!raw) return null; - - // Already a full datetime — trust it and ignore the separate time field. - if (raw.includes("T") || /\s\d{1,2}:\d{2}/.test(raw)) { - const d = new Date(raw); - return Number.isNaN(d.getTime()) ? null : d; - } - - const cleanTime = (time ?? "").trim(); - const iso = cleanTime ? `${raw}T${cleanTime}` : raw; - const d = new Date(iso); - return Number.isNaN(d.getTime()) ? null : d; + return parseIranLocalDateTime(date, time); } /**