backend requirements onthe xustomer app

This commit is contained in:
2026-09-07 10:55:11 +05:30
parent 35675d8a9b
commit 1b2690b21a
56 changed files with 13342 additions and 1071 deletions

103
utils/epoch.go Normal file
View File

@@ -0,0 +1,103 @@
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"))
}