package utils import ( "fmt" "time" ) // Timestamps on the customer surface. // // The requirement is epoch milliseconds, UTC, integer — a real instant, not a // wall clock. That is not what this database hands back. The DSN sets // TimeZone=Asia/Kolkata and the timestamp columns hold IST wall-clock digits // (see DBNow), so a stored value read into a time.Time carries the right // digits with the wrong (or no) zone. Calling UnixMilli on it directly is off // by 5h30m — the same class of defect as sending a naive local string with a Z // on it, which has already produced "yesterday's work shown as today" on the // miler app. So every timestamp leaving /customer/* goes through here. // // Reinterpreting the wall clock in IST is correct for both shapes this // database produces: a value tagged UTC that actually holds IST digits, and a // value correctly tagged +05:30. Both name the same instant afterwards. // istLocation is Asia/Kolkata, with an exact fixed-offset fallback for // containers shipped without tzdata. IST observes no DST, so +05:30 is not an // approximation. var istLocation = func() *time.Location { if loc, err := time.LoadLocation("Asia/Kolkata"); err == nil { return loc } return time.FixedZone("IST", 5*3600+30*60) }() // IST reinterprets a database timestamp's wall clock as Indian Standard Time, // yielding the instant it actually names. func IST(t time.Time) time.Time { return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), istLocation) } // EpochMillis converts a database timestamp to UTC epoch milliseconds. func EpochMillis(t time.Time) int64 { return IST(t).UnixMilli() } // EpochMillisPtr is EpochMillis over a nullable column. A nil timestamp stays // null in JSON rather than becoming the epoch, which the client would render // as 1 Jan 1970. func EpochMillisPtr(t *time.Time) *int64 { if t == nil || t.IsZero() { return nil } ms := EpochMillis(*t) return &ms } // ISTNow is the current moment in IST, for formatting and for comparing // against a value already put through IST(). func ISTNow() time.Time { return time.Now().In(istLocation) } // ISTLocation exposes the zone for callers building their own times. func ISTLocation() *time.Location { return istLocation } // FormatISTDate renders a date the way the customer app shows it: "Thu, 12 Sep". // Display strings are formatted server-side, in IST, so the client never has to // know the operating timezone. func FormatISTDate(t time.Time) string { return IST(t).Format("Mon, 2 Jan") } // FormatISTDay renders a date relative to today where that reads better — // "Today", "Tomorrow", otherwise "Mon, 8 Sep". Used for pickup slot days. func FormatISTDay(t time.Time) string { d := IST(t) today := ISTNow() y1, m1, d1 := d.Date() y2, m2, d2 := today.Date() switch { case y1 == y2 && m1 == m2 && d1 == d2: return "Today" case y1 == y2 && m1 == m2 && d1 == d2+1: return "Tomorrow" default: tomorrow := today.AddDate(0, 0, 1) y3, m3, d3 := tomorrow.Date() if y1 == y3 && m1 == m3 && d1 == d3 { return "Tomorrow" } return d.Format("Mon, 2 Jan") } } // FormatISTWindow renders a pickup window as the design writes it: // "2:00 – 4:00 PM" — en dash, spaced, and the meridiem stated once when both // ends share it. func FormatISTWindow(from, to time.Time) string { f, t := IST(from), IST(to) fMer, tMer := f.Format("PM"), t.Format("PM") if fMer == tMer { return fmt.Sprintf("%s – %s", f.Format("3:04"), t.Format("3:04 PM")) } return fmt.Sprintf("%s – %s", f.Format("3:04 PM"), t.Format("3:04 PM")) }