refactor(config): remove price URL env overrides

This commit is contained in:
2026-06-29 09:23:20 +07:00
parent d9c7421c7e
commit 20dc03c6d2
11 changed files with 47 additions and 84 deletions
+2 -1
View File
@@ -39,4 +39,5 @@ WC_FOOTBALL_DATA_TOKEN=
# PORT — defaults to 8080 (internal health server)
# TELEGRAM_WEBHOOK_SECRET — long polling has no webhook
# GOLD_VNAPP_API_KEY — gold module auto-fetches + caches the key to Mongo
# STOCK/COIN/GOLD *_API_URL overrides — modules use their coded default providers
# Stock/coin/gold URL env overrides are not supported; modules use coded
# default providers.
+20 -50
View File
@@ -86,13 +86,6 @@ func main() {
if cfg.TelegramBotToken == "" {
log.Fatal("missing required env", "key", "TELEGRAM_BOT_TOKEN")
}
exportOptionalEnv("GOLD_PRICE_API_URL", cfg.GoldPriceAPIURL)
exportOptionalEnv("GOLD_FX_API_URL", cfg.GoldFXAPIURL)
exportOptionalEnv("GOLD_VNAPP_API_URL", cfg.GoldVNAppAPIURL)
exportOptionalEnv("GOLD_VNAPP_API_KEY", cfg.GoldVNAppAPIKey)
exportOptionalEnv("COIN_BINANCE_API_URL", cfg.CoinBinanceAPIURL)
exportOptionalEnv("COIN_COINBASE_API_URL", cfg.CoinCoinbaseAPIURL)
exportOptionalEnv("COIN_COINGECKO_API_URL", cfg.CoinCoinGeckoAPIURL)
// Periodic metrics flush. Cancels with rootCtx and emits one final
// flush on shutdown so the trailing window isn't lost.
@@ -264,23 +257,16 @@ func buildProvider(ctx context.Context, cfg config) (storage.Provider, func(), e
}
type config struct {
Port string
TelegramBotToken string
SourceCommit string // Coolify-injected commit SHA (runtime env) for deploynotify
GeminiAPIKey string
GoldPriceAPIURL string
GoldFXAPIURL string
GoldVNAppAPIURL string
GoldVNAppAPIKey string
CoinBinanceAPIURL string
CoinCoinbaseAPIURL string
CoinCoinGeckoAPIURL string
Modules []string
BotOwnerID int64
AdminUserIDs map[int64]bool
KVProvider string // empty = auto-detect; or "memory"|"mongodb"
MongoURL string // required when KVProvider=mongodb (Atlas SRV connection string; SECRET — never log)
MongoDatabase string // required when KVProvider=mongodb
Port string
TelegramBotToken string
SourceCommit string // Coolify-injected commit SHA (runtime env) for deploynotify
GeminiAPIKey string
Modules []string
BotOwnerID int64
AdminUserIDs map[int64]bool
KVProvider string // empty = auto-detect; or "memory"|"mongodb"
MongoURL string // required when KVProvider=mongodb (Atlas SRV connection string; SECRET — never log)
MongoDatabase string // required when KVProvider=mongodb
}
func loadConfig() config {
@@ -301,32 +287,16 @@ func loadConfig() config {
log.Fatal("invalid PORT", "value", port)
}
return config{
Port: port,
TelegramBotToken: envMap["TELEGRAM_BOT_TOKEN"],
SourceCommit: envMap["SOURCE_COMMIT"],
GeminiAPIKey: envMap["GEMINI_API_KEY"],
GoldPriceAPIURL: envMap["GOLD_PRICE_API_URL"],
GoldFXAPIURL: envMap["GOLD_FX_API_URL"],
GoldVNAppAPIURL: envMap["GOLD_VNAPP_API_URL"],
GoldVNAppAPIKey: envMap["GOLD_VNAPP_API_KEY"],
CoinBinanceAPIURL: envMap["COIN_BINANCE_API_URL"],
CoinCoinbaseAPIURL: envMap["COIN_COINBASE_API_URL"],
CoinCoinGeckoAPIURL: envMap["COIN_COINGECKO_API_URL"],
Modules: splitCSV(envMap["MODULES"]),
BotOwnerID: parseInt64(envMap["OWNER_ID"]),
AdminUserIDs: parseInt64Set(envMap["ADMIN_IDS"]),
KVProvider: envMap["KV_PROVIDER"],
MongoURL: envMap["MONGO_URL"],
MongoDatabase: envMap["MONGO_DATABASE"],
}
}
func exportOptionalEnv(key, value string) {
if strings.TrimSpace(value) == "" {
return
}
if err := os.Setenv(key, value); err != nil {
log.Warn("could not export optional env", "key", key, "err", err)
Port: port,
TelegramBotToken: envMap["TELEGRAM_BOT_TOKEN"],
SourceCommit: envMap["SOURCE_COMMIT"],
GeminiAPIKey: envMap["GEMINI_API_KEY"],
Modules: splitCSV(envMap["MODULES"]),
BotOwnerID: parseInt64(envMap["OWNER_ID"]),
AdminUserIDs: parseInt64Set(envMap["ADMIN_IDS"]),
KVProvider: envMap["KV_PROVIDER"],
MongoURL: envMap["MONGO_URL"],
MongoDatabase: envMap["MONGO_DATABASE"],
}
}
+4 -3
View File
@@ -24,9 +24,10 @@ services:
# PORT defaults to 8080 (internal health server) — omit unless overriding.
# Long polling = no TELEGRAM_WEBHOOK_SECRET, no /webhook, no public domain.
# Cron is in-process only — there is no /cron HTTP route and no secret.
# No stock/coin/gold *_API_URL overrides — modules use their coded default
# providers (stock: SSI/VCI/KBS; coin: BinanceCoinbaseCoinGecko;
# gold: VNAppMob→spot). GOLD_VNAPP_API_KEY auto-fetches + caches to Mongo.
# No stock/coin/gold URL env overrides — modules use their coded default
# providers (stock: SSI/VCI/KBS; coin: Binance->Coinbase->CoinGecko;
# gold: VNAppMob). If GOLD_VNAPP_API_KEY is unset, the bot auto-fetches
# and caches a key to Mongo.
# Long polling is outbound-only: nothing inbound to route, so no published
# ports and no public domain. `expose` keeps :8080 reachable inside the
# Coolify network for the container health monitor against GET / only.
+2 -2
View File
@@ -37,8 +37,8 @@ Copy [`.env.example`](../.env.example) → `.env` (gitignored) and fill in.
| `WC_FOOTBALL_DATA_TOKEN` | optional | football-data.org token for the `wc` module |
**Leave UNSET on self-host:** `KV_PROVIDER`, `PORT`,
`TELEGRAM_WEBHOOK_SECRET`, `GOLD_VNAPP_API_KEY`, and the `STOCK/COIN/GOLD
*_API_URL` overrides (modules use coded defaults).
`TELEGRAM_WEBHOOK_SECRET`, and `GOLD_VNAPP_API_KEY`. Stock, coin, and gold URL
overrides are not supported in runtime env; modules use coded defaults.
> Cron runs in-process (`internal/cron`) — there is no `/cron` HTTP route and no
> `CRON_SHARED_SECRET`. The scheduler is the sole trigger; nothing inbound.
+1 -1
View File
@@ -28,7 +28,7 @@ type state struct {
}
func newState(store Store) *state {
return &state{store: store, prices: NewPriceClientFromEnv()}
return &state{store: store, prices: NewPriceClient()}
}
func (s *state) now() time.Time {
+4 -5
View File
@@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"net/http"
"os"
"sync"
"time"
)
@@ -47,13 +46,13 @@ type cachedPrice struct {
expiry time.Time
}
func NewPriceClientFromEnv() *PriceClient {
func NewPriceClient() *PriceClient {
httpClient := &http.Client{Timeout: coinHTTPTimeout}
return &PriceClient{
Providers: []PriceProvider{
&BinanceProvider{HTTP: httpClient, URL: os.Getenv("COIN_BINANCE_API_URL")},
&CoinbaseProvider{HTTP: httpClient, URL: os.Getenv("COIN_COINBASE_API_URL")},
&CoinGeckoProvider{HTTP: httpClient, URL: os.Getenv("COIN_COINGECKO_API_URL")},
&BinanceProvider{HTTP: httpClient},
&CoinbaseProvider{HTTP: httpClient},
&CoinGeckoProvider{HTTP: httpClient},
},
CacheTTL: coinPriceCacheTTL,
}
+3 -3
View File
@@ -18,9 +18,9 @@ type compositePriceFetcher struct {
vnappmob sjcPriceFetcher
}
// NewCompositePriceFetcherFromEnv builds the production price fetcher using
// only the env-driven VNAppMob client.
func NewCompositePriceFetcherFromEnv(coll storage.Collection) priceFetcher {
// NewCompositePriceFetcher builds the production price fetcher using the coded
// VNAppMob endpoint.
func NewCompositePriceFetcher(coll storage.Collection) priceFetcher {
return &compositePriceFetcher{
vnappmob: NewVNAppMobClientFromEnv(coll),
}
@@ -91,8 +91,8 @@ func TestGoldPriceLines(t *testing.T) {
}
}
func TestNewCompositePriceFetcherFromEnv(t *testing.T) {
f, ok := NewCompositePriceFetcherFromEnv(storage.NewMemoryProvider().Collection("gold")).(*compositePriceFetcher)
func TestNewCompositePriceFetcher(t *testing.T) {
f, ok := NewCompositePriceFetcher(storage.NewMemoryProvider().Collection("gold")).(*compositePriceFetcher)
if !ok {
t.Fatalf("expected *compositePriceFetcher, got %T", f)
}
+1 -1
View File
@@ -33,7 +33,7 @@ type state struct {
func newState(coll storage.Collection) *state {
return &state{
store: storage.Typed[Portfolio](coll),
prices: NewCompositePriceFetcherFromEnv(coll),
prices: NewCompositePriceFetcher(coll),
}
}
+2 -9
View File
@@ -7,7 +7,6 @@ import (
"fmt"
"math"
"net/http"
"os"
"strings"
"sync"
"time"
@@ -42,7 +41,8 @@ type SJCPrice struct {
type GoldPriceClient struct {
HTTP *http.Client
// Per-provider URL overrides; empty means the provider default.
// URL fields are explicit injection points for tests; production uses the
// provider defaults.
GoldURL string // primary: gold-api.com
SwissquoteURL string
NBPURL string
@@ -57,13 +57,6 @@ type GoldPriceClient struct {
fxExpiry time.Time
}
func NewGoldPriceClientFromEnv() *GoldPriceClient {
return &GoldPriceClient{
GoldURL: strings.TrimSpace(os.Getenv("GOLD_PRICE_API_URL")),
FXURL: strings.TrimSpace(os.Getenv("GOLD_FX_API_URL")),
}
}
func (c *GoldPriceClient) FetchPrice(ctx context.Context) (GoldPrice, error) {
xauUSD, err := c.fetchXAUUSD(ctx)
if err != nil {
+6 -7
View File
@@ -36,7 +36,7 @@ type apiKeyCache struct {
// refreshing it before expiry or when the SJC endpoint returns 403.
type VNAppMobClient struct {
HTTP *http.Client
BaseURL string // optional override; default https://api.vnappmob.com
BaseURL string // explicit test override; production uses https://api.vnappmob.com
Token string // optional env override (GOLD_VNAPP_API_KEY)
cache storage.DocStore[apiKeyCache] // module-scoped typed cache store
@@ -44,14 +44,13 @@ type VNAppMobClient struct {
mu sync.Mutex
}
// NewVNAppMobClientFromEnv creates a client reading GOLD_VNAPP_API_URL and
// GOLD_VNAPP_API_KEY from the environment. When the key is empty, the client
// refreshes it automatically via the VNAppMob refresh endpoint.
// NewVNAppMobClientFromEnv creates a client reading only GOLD_VNAPP_API_KEY
// from the environment. When the key is empty, the client refreshes it
// automatically via the coded VNAppMob endpoint.
func NewVNAppMobClientFromEnv(coll storage.Collection) *VNAppMobClient {
return &VNAppMobClient{
BaseURL: strings.TrimSpace(os.Getenv("GOLD_VNAPP_API_URL")),
Token: strings.TrimSpace(os.Getenv("GOLD_VNAPP_API_KEY")),
cache: storage.Typed[apiKeyCache](coll),
Token: strings.TrimSpace(os.Getenv("GOLD_VNAPP_API_KEY")),
cache: storage.Typed[apiKeyCache](coll),
}
}