133 lines
3.3 KiB
Go
133 lines
3.3 KiB
Go
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: <data>"
|
||
ExpiryNote string // "La quotazione dei costi è valida sino a <data> (≈ ... 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
|
||
}
|