import { Result } from "@repo/rdx-utils"; import { z } from "zod/v4"; import { translateZodValidationError } from "../helpers"; import { ValueObject } from "./value-object"; interface UtcDateProps { value: string; } const UTC_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/; export class UtcDate extends ValueObject { private readonly timestamp: number; private constructor(props: UtcDateProps) { super(props); this.timestamp = UtcDate.toTimestamp(props.value); } static create(dateString: string): Result { const validation = UtcDate.validate(dateString); if (!validation.success) { return Result.fail(translateZodValidationError("UtcDate creation failed", validation.error)); } return Result.ok(new UtcDate({ value: validation.data })); } static createFromISO(dateString: string): Result { return UtcDate.create(dateString); } static today(): UtcDate { const now = new Date(); const year = now.getUTCFullYear(); const month = String(now.getUTCMonth() + 1).padStart(2, "0"); const day = String(now.getUTCDate()).padStart(2, "0"); return new UtcDate({ value: `${year}-${month}-${day}`, }); } static validate(dateString: string) { return z .string() .regex(UTC_DATE_REGEX, "Invalid YYYY-MM-DD format") .superRefine((value, ctx) => { if (!UtcDate.isValidCalendarDate(value)) { ctx.addIssue({ code: "custom", message: "Invalid calendar date", }); } }) .safeParse(dateString); } private static isValidCalendarDate(value: string): boolean { const [yearRaw, monthRaw, dayRaw] = value.split("-"); const year = Number(yearRaw); const month = Number(monthRaw); const day = Number(dayRaw); const date = new Date(Date.UTC(year, month - 1, day)); return ( date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day ); } private static toTimestamp(value: string): number { const [yearRaw, monthRaw, dayRaw] = value.split("-"); const year = Number(yearRaw); const month = Number(monthRaw); const day = Number(dayRaw); return Date.UTC(year, month - 1, day, 0, 0, 0, 0); } getProps(): UtcDateProps { return this.props; } toPrimitive(): string { return this.props.value; } toDateString(): string { return this.props.value; } toString(): string { return this.toDateString(); } toISOString(): string { return `${this.props.value}T00:00:00.000Z`; } toDate(): Date { return new Date(this.timestamp); } toEuropeanString(): string { const [year, month, day] = this.props.value.split("-"); return `${day}/${month}/${year}`; } equals(other: UtcDate): boolean { return this.props.value === other.props.value; } isFuture(currentDate: UtcDate = UtcDate.today()): boolean { return this.timestamp > currentDate.timestamp; } isPast(currentDate: UtcDate = UtcDate.today()): boolean { return this.timestamp < currentDate.timestamp; } isToday(): boolean { return this.equals(UtcDate.today()); } isBefore(other: UtcDate): boolean { return this.timestamp < other.timestamp; } isAfter(other: UtcDate): boolean { return this.timestamp > other.timestamp; } }