package zenmoney import ( "encoding/json" "fmt" "strings" "time" ) // Fact keys the poller writes, all under source "poll:zenmoney". Two windows, // because they are the two questions he actually asks; a per-category // breakdown would mean storing what he bought, and the store is not a ledger. const ( KeySpentToday = "money_today" KeySpentMonth = "money_month" ) // Source — the provenance every money fact carries. The loop's rules trust // source, and nothing in Maven has a rule on these keys: they are read when he // asks, never a reason to speak. Maven is not a nag, least of all about money. const Source = "poll:zenmoney" // FactValue — the JSON stored in a money fact. A wire shape of its own rather // than the Summary struct so From/To (which carry a timezone and a clock) stay // out of the store; the key already says which window it is. type FactValue struct { Spent []Money `json:"spent"` Earned []Money `json:"earned"` Count int `json:"count"` } // Value encodes the summary for the facts table. Returns ok=false for an empty // summary: no transactions read means no fact written, so that a failed or // empty poll can never be recited back to him as a zero. func (s Summary) Value() (string, bool) { if s.Empty() { return "", false } b, err := json.Marshal(FactValue{Spent: s.Spent, Earned: s.Earned, Count: s.Count}) if err != nil { return "", false } return string(b), true } // ParseFactValue decodes a stored money fact. func ParseFactValue(raw string) (FactValue, error) { var v FactValue if err := json.Unmarshal([]byte(raw), &v); err != nil { return FactValue{}, err } return v, nil } // FormatRU renders a money fact the way Maven says it — feminine, informal, // and only about numbers that came from ZenMoney. window is the Russian phrase // for the period ("сегодня", "в этом месяце"). // // No commentary. She reports the figure and stops: an opinion about his // spending is exactly the nagging Maven is not for. func (v FactValue) FormatRU(window string) string { if v.Count == 0 { return "" } var parts []string if len(v.Spent) > 0 { parts = append(parts, "потратил "+joinMoney(v.Spent)) } if len(v.Earned) > 0 { parts = append(parts, "получил "+joinMoney(v.Earned)) } if len(parts) == 0 { return "" } return window + " ты " + strings.Join(parts, ", ") + "." } func joinMoney(ms []Money) string { parts := make([]string, 0, len(ms)) for _, m := range ms { parts = append(parts, fmt.Sprintf("%s %s", formatAmount(m.Amount), m.Currency)) } return strings.Join(parts, " и ") } // formatAmount — whole units when the amount is whole, two decimals otherwise. // Never rounded to something prettier than the truth. func formatAmount(a float64) string { if a == float64(int64(a)) { return fmt.Sprintf("%d", int64(a)) } return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.2f", a), "0"), ".") } // StaleAfter — how old a money fact may be and still be worth reciting. The // poller is off unless configured and can be down; answering with last week's // total as if it were today's would be a lie by omission, so a stale fact is // reported as stale. const StaleAfter = 26 * time.Hour