forked from Yara724/api
Fixed timing issue with UTC
This commit is contained in:
26
src/helpers/iran-datetime.spec.ts
Normal file
26
src/helpers/iran-datetime.spec.ts
Normal file
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
82
src/helpers/iran-datetime.ts
Normal file
82
src/helpers/iran-datetime.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 { StepsEnum } from "src/Types&Enums/blame-request-management/steps.enum";
|
||||||
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
|
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
|
||||||
import { UserDbService } from "src/users/entities/db-service/user.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 { AutoCloseRequestService } from "src/utils/cron/cron.service";
|
||||||
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
|
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
|
||||||
import {
|
import {
|
||||||
@@ -395,32 +396,16 @@ export class RequestManagementService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Best-effort parser that combines a date input with an optional `HH:MM`
|
* 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
|
* time in Iran (Asia/Tehran, UTC+3:30). Accepts a `Date`, an ISO datetime
|
||||||
* string so DTO payloads (which currently send `"YYYY-MM-DD"` + `"HH:MM"`)
|
* string, or an ISO date-only string so DTO payloads (which send
|
||||||
* can be passed through directly. Returns `null` when the result is not a
|
* `"YYYY-MM-DD"` + `"HH:MM"` in local Iran time) can be passed through
|
||||||
* finite instant.
|
* directly. Returns `null` when the result is not a finite instant.
|
||||||
*/
|
*/
|
||||||
private parseAccidentInstant(
|
private parseAccidentInstant(
|
||||||
date: Date | string,
|
date: Date | string,
|
||||||
time?: string,
|
time?: string,
|
||||||
): Date | null {
|
): Date | null {
|
||||||
if (date instanceof Date) {
|
return parseIranLocalDateTime(date, time);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user