mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
Add housing/pets/Thom Krooke, wordle overhaul, boost system, backup strategy
Part 3 (Housing, Thom Krooke & Pets): - Housing system with tiered upgrades, mortgage loans, FRED API rates - Thom Krooke realtor NPC with !thom commands and !thom pay extra principal - Pet system with arrival, naming, leveling, combat, morning defense - Pet ditch recovery scales with level, name validation Wordle overhaul: - Remove daily expiration — puzzles persist until solved/failed - Auto-start new puzzle after solve, fail, or skip - Sequential puzzle IDs instead of date-based - Auto-create puzzle on first guess if none active Adventure fixes: - Double XP/money boost toggle (!adv boost, admin only) - Fix ensureCharacter wiping existing data on query errors - Fix rival RPS format string bug - Fix daily summary empty data (load today+yesterday logs) - Fix holdem DM-to-room reply routing - Fix Robbie payout to 25% of item value - Add fishing to leaderboard and TwinBee calculations - Add Thom to morning menu - Persist mortgage rate across restarts via db.CacheGet/Set Infrastructure: - Daily database backup via VACUUM INTO with 7-day retention - Admin DM notification on backup failure (async) - Daily NPC house balance reset for holdem Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
158
internal/plugin/adventure_mortgage.go
Normal file
158
internal/plugin/adventure_mortgage.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gogobee/internal/db"
|
||||
)
|
||||
|
||||
// ── FRED API Integration ───────────────────────────────────────────────────
|
||||
//
|
||||
// Pulls the 5/1 ARM rate weekly from FRED (series: MORTGAGE5US).
|
||||
// Requires FRED_API_KEY environment variable.
|
||||
// Falls back to a hardcoded default if the API is unavailable.
|
||||
|
||||
const fredSeries = "MORTGAGE5US"
|
||||
const defaultMortgageRate = 6.5
|
||||
const currentRateCacheKey = "mortgage_current_rate"
|
||||
|
||||
type fredResponse struct {
|
||||
Observations []struct {
|
||||
Date string `json:"date"`
|
||||
Value string `json:"value"`
|
||||
} `json:"observations"`
|
||||
}
|
||||
|
||||
// fetchFREDRate pulls the latest 5/1 ARM rate from the FRED API.
|
||||
func fetchFREDRate() (float64, error) {
|
||||
apiKey := os.Getenv("FRED_API_KEY")
|
||||
if apiKey == "" {
|
||||
return 0, fmt.Errorf("FRED_API_KEY not set")
|
||||
}
|
||||
|
||||
url := fmt.Sprintf(
|
||||
"https://api.stlouisfed.org/fred/series/observations?series_id=%s&api_key=%s&file_type=json&sort_order=desc&limit=1",
|
||||
fredSeries, apiKey,
|
||||
)
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("FRED API request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return 0, fmt.Errorf("FRED API status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var data fredResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return 0, fmt.Errorf("FRED JSON decode: %w", err)
|
||||
}
|
||||
|
||||
if len(data.Observations) == 0 {
|
||||
return 0, fmt.Errorf("FRED: no observations returned")
|
||||
}
|
||||
|
||||
val := data.Observations[0].Value
|
||||
if val == "." {
|
||||
return 0, fmt.Errorf("FRED: value is placeholder")
|
||||
}
|
||||
|
||||
rate, err := strconv.ParseFloat(val, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("FRED: parse rate %q: %w", val, err)
|
||||
}
|
||||
|
||||
// Clamp to sane range
|
||||
if rate < 1.0 {
|
||||
rate = 1.0
|
||||
}
|
||||
if rate > 15.0 {
|
||||
rate = 15.0
|
||||
}
|
||||
|
||||
return rate, nil
|
||||
}
|
||||
|
||||
// getCurrentMortgageRate returns the cached rate, or fetches fresh if stale (daily).
|
||||
func getCurrentMortgageRate() float64 {
|
||||
// Check DB cache (24h TTL)
|
||||
if val := db.CacheGet(currentRateCacheKey, 24*3600); val != "" {
|
||||
if rate, err := strconv.ParseFloat(val, 64); err == nil {
|
||||
return rate
|
||||
}
|
||||
}
|
||||
|
||||
rate, err := fetchFREDRate()
|
||||
if err != nil {
|
||||
slog.Warn("mortgage: FRED fetch failed, using default", "err", err)
|
||||
return defaultMortgageRate
|
||||
}
|
||||
|
||||
db.CacheSet(currentRateCacheKey, strconv.FormatFloat(rate, 'f', 2, 64))
|
||||
return rate
|
||||
}
|
||||
|
||||
// ── Weekly Mortgage Ticker ─────────────────────────────────────────────────
|
||||
|
||||
func (p *AdventurePlugin) mortgageTicker() {
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
now := time.Now().UTC()
|
||||
// Run on Mondays only (FRED updates Thursdays, we process Mondays)
|
||||
if now.Weekday() != time.Monday {
|
||||
continue
|
||||
}
|
||||
|
||||
_, week := now.ISOWeek()
|
||||
dateKey := fmt.Sprintf("%d-W%02d", now.Year(), week)
|
||||
jobName := "mortgage_weekly"
|
||||
if db.JobCompleted(jobName, dateKey) {
|
||||
continue
|
||||
}
|
||||
|
||||
slog.Info("mortgage: running weekly payment processing")
|
||||
|
||||
// Fetch fresh rate and compare with previous
|
||||
newRate := getCurrentMortgageRate()
|
||||
oldRate := p.getLastKnownRate()
|
||||
|
||||
if oldRate > 0 && oldRate != newRate {
|
||||
p.sendMortgageRateChangeDMs(oldRate, newRate)
|
||||
}
|
||||
p.setLastKnownRate(newRate)
|
||||
|
||||
// Process payments
|
||||
p.processMortgagePayments()
|
||||
|
||||
db.MarkJobCompleted(jobName, dateKey)
|
||||
}
|
||||
}
|
||||
|
||||
const lastKnownRateCacheKey = "mortgage_last_known_rate"
|
||||
|
||||
func (p *AdventurePlugin) getLastKnownRate() float64 {
|
||||
val := db.CacheGet(lastKnownRateCacheKey, 365*24*3600) // 1 year TTL
|
||||
if val == "" {
|
||||
return 0
|
||||
}
|
||||
rate, err := strconv.ParseFloat(val, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return rate
|
||||
}
|
||||
|
||||
func (p *AdventurePlugin) setLastKnownRate(rate float64) {
|
||||
db.CacheSet(lastKnownRateCacheKey, strconv.FormatFloat(rate, 'f', 2, 64))
|
||||
}
|
||||
Reference in New Issue
Block a user