Last month, a merchant in Jakarta complained that their settlement report showed payments from “tomorrow” — a full day ahead. The amounts were correct, but the dates were wrong. Every single transaction was timestamped in UTC, but the report expected WIB (UTC+7).
The fix was a one-line change. The journey to finding it? Three days.
How it happened
Our reconciliation pipeline aggregated transactions from five different PSPs. Each one had a different idea about what “timestamp” meant:
- Mandiri VA: WIB, no timezone annotation
- BRI VA: UTC, explicitly marked
- Winpay: Local server time (whatever that meant)
- OVO: Unix epoch, implicitly UTC
- LinkAja: ISO 8601 with offset
We normalized everything to time.Time on ingest:
func parseTimestamp(raw string, source string) (time.Time, error) {
switch source {
case "mandiri":
return parseWIB(raw)
case "bri":
return time.Parse(time.RFC3339, raw)
// ...
}
}
Seemed fine. The database stored UTC. The API returned UTC. Time arithmetic worked. Tests passed.
The silent killer: Truncate
The bug was in the settlement report generator. To group transactions by settlement date, we truncated timestamps to the start of the day:
func settlementDate(t time.Time) time.Time {
return t.Truncate(24 * time.Hour)
}
Truncate operates in the time zone embedded in the time.Time value. Since everything
was in UTC, it truncated to UTC midnight. A transaction at 11 PM WIB (4 PM UTC) was
correctly assigned to the same WIB day, but the report header said “Settlement Date:
June 15, 2026” while the WIB date was June 16.
The fix
func settlementDate(t time.Time, loc *time.Location) time.Time {
local := t.In(loc)
return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, loc)
}
Explicit. Deliberate. No magic.
What I learned
time.Time is a great type, but it carries an implicit timezone that makes operations
like Truncate, AddDate, and comparisons subtly wrong for business logic. Always be
explicit about the timezone when doing date-level operations.
And always, always test with a clock set to a different timezone than your server’s.