package main import ( "fmt" "os" "strings" "time" ) // MessageBundle è l’UNICA struct che viaggia fino a Matrix. // Metti qui TUTTO ciò che serve per il messaggio finale. type MessageBundle struct { // righe pronte (se vuote, Compose() le omette) DecisionLine string // es. "consiglio: azione=..., motivo=..." Instructions string // es. "ISTRUZIONI MANUALI: esegui swap ..." // scadenza del quote (arriva dalla stessa chiamata THOR che dà fee/expected) QuoteValidUntil time.Time // opzionali (se vuote, Compose() le genera in base a QuoteValidUntil) ValidityLine string // "Suggerimento valido sino a: " ExpiryNote string // "La quotazione dei costi è valida sino a (≈ ... restanti) ..." } // Compose costruisce il testo da inviare su Matrix. // Garanzie: // - La riga "Suggerimento valido sino a: ..." è SEMPRE presente. // - Se QuoteValidUntil è zero, usa fallback now+24h nella TZ utente. func (mb MessageBundle) Compose() string { var b strings.Builder appendLn := func(s string) { s = strings.TrimSpace(s) if s == "" { return } if b.Len() > 0 { b.WriteByte('\n') } b.WriteString(s) } // 1) righe “grezze” (se presenti) appendLn(mb.DecisionLine) appendLn(mb.Instructions) // 2) calcolo scadenza effettiva + riga "Suggerimento valido sino a: ..." vu := mb.QuoteValidUntil if vu.IsZero() { vu = time.Now().UTC().Add(24 * time.Hour) } vline := strings.TrimSpace(mb.ValidityLine) if vline == "" { vline = buildValidityLine(vu) } appendLn(vline) // 3) nota esplicativa con tempo residuo (se non fornita) note := strings.TrimSpace(mb.ExpiryNote) if note == "" { note = buildExpiryNote(vu) } appendLn(note) return b.String() } // ====== Formattazione scadenza/durate centralizzata qui ====== func buildValidityLine(validUntil time.Time) string { loc := userLocation() return "Suggerimento valido sino a: " + validUntil.In(loc).Format("2006-01-02 15:04:05 MST") } func buildExpiryNote(validUntil time.Time) string { if validUntil.IsZero() { return "" } loc := userLocation() now := time.Now().In(loc) local := validUntil.In(loc) remaining := local.Sub(now) if remaining <= 0 { return fmt.Sprintf("ATTENZIONE: la quotazione dei costi è scaduta il %s. Dopo questa data, i costi potrebbero cambiare.", local.Format("2006-01-02 15:04:05 MST")) } return fmt.Sprintf("La quotazione dei costi è valida sino a %s (≈ %s restanti). Dopo questadata, i costi potrebbero cambiare.", local.Format("2006-01-02 15:04:05 MST"), humanDuration(remaining)) } func humanDuration(d time.Duration) string { if d < 0 { d = -d } sec := int64(d.Seconds()) if sec < 60 { return fmt.Sprintf("%ds", sec) } min := sec / 60 if min < 60 { rem := sec % 60 if rem == 0 { return fmt.Sprintf("%dm", min) } return fmt.Sprintf("%dm %ds", min, rem) } h := min / 60 m := min % 60 if h < 24 { if m == 0 { return fmt.Sprintf("%dh", h) } return fmt.Sprintf("%dh %dm", h, m) } days := h / 24 hh := h % 24 if hh == 0 && m == 0 { return fmt.Sprintf("%dd", days) } if m == 0 { return fmt.Sprintf("%dd %dh", days, hh) } return fmt.Sprintf("%dd %dh %dm", days, hh, m) } func userLocation() *time.Location { if tz := strings.TrimSpace(os.Getenv("TZ")); tz != "" { if loc, err := time.LoadLocation(tz); err == nil { return loc } } return time.Local }