diff --git a/plans/phase-01-vnappmob-api-research-and-key-refresh-design.md b/plans/phase-01-vnappmob-api-research-and-key-refresh-design.md new file mode 100644 index 0000000..265740b --- /dev/null +++ b/plans/phase-01-vnappmob-api-research-and-key-refresh-design.md @@ -0,0 +1,64 @@ +--- +phase: 1 +title: VNAppMob API research and key refresh design +status: completed +priority: P1 +effort: 1h +dependencies: [] +--- + +# Phase 1: VNAppMob API research and key refresh design + +## Overview + +Confirm the exact request/response contracts for API key refresh and SJC price fetch, decide where the key is persisted, and define the expiry detection strategy. + +## Requirements + +- **Functional**: Document how to refresh the free API key, how to call `GET /api/v2/gold/sjc`, and the JSON response shape. +- **Non-functional**: Use only built-in Go packages for JWT claim extraction if possible; avoid new dependencies. + +## Architecture + +1. **Key refresh endpoint**: `POST https://api.vnappmob.com/api/request_api_key?scope=gold` + - Returns a raw JWT string. + - JWT payload contains `exp` (Unix seconds), `scope`, `permission`. +2. **SJC price endpoint**: `GET https://api.vnappmob.com/api/v2/gold/sjc` + - Header: `Authorization: Bearer ` + - Response: `{"results":[{"buy_1l":,"sell_1l":, ...}]}` + - Use the first element's `buy_1l`/`sell_1l` as the spot price. +3. **Key storage**: + - KV key: `vnappmob:api_key` + - Value JSON: `{"token":"","exp":}` + - Stored under the gold module's prefixed KV (already `gold:`). +4. **Expiry detection**: + - Parse JWT payload (middle segment), base64-url decode, JSON-decode `exp`. + - Refresh when `exp - now < refreshBuffer` (buffer e.g. 1h or 1 day). + - If parsing fails, force refresh. + +## Related Code Files + +- Read: `internal/modules/gold/prices.go` +- Read: `internal/modules/gold/price_providers.go` +- Read: `internal/modules/gold/price_urls.go` +- Read: `internal/storage/kv_store.go` + +## Implementation Steps + +1. Re-fetch `https://api.vnappmob.com/api/request_api_key?scope=gold` to confirm response format and inspect a fresh JWT via `jwt.io` or a small Go snippet. +2. Decode JWT payload and verify fields (`exp`, `iat`, `scope`, `permission`). +3. Document the response sample in this phase file. +4. Choose buffer: 24h (refresh one day before expiry, avoiding midnight edge cases). +5. Decide concurrency strategy: CAS lock key `vnappmob:refresh_lock` or accept last-write-wins with 1-min TTL. + +## Success Criteria + +- [x] API refresh endpoint verified to return a JWT string. +- [x] JWT payload fields and expiry semantics documented. +- [x] SJC endpoint response shape documented with sample values. +- [x] KV key names and refresh buffer decided. + +## Risk Assessment + +- **Risk**: Refresh endpoint returns non-JWT or changes shape. **Mitigation**: treat non-JWT as an error and fall back to existing providers. +- **Risk**: Clock skew causes premature expiry. **Mitigation**: 24h buffer absorbs skew; refresh on any 403 even if expiry appears valid. diff --git a/plans/phase-02-implement-sjc-price-client-with-api-key-management.md b/plans/phase-02-implement-sjc-price-client-with-api-key-management.md new file mode 100644 index 0000000..864279e --- /dev/null +++ b/plans/phase-02-implement-sjc-price-client-with-api-key-management.md @@ -0,0 +1,76 @@ +--- +phase: 2 +title: Implement SJC price client with API key management +status: completed +priority: P1 +effort: 3h +dependencies: + - 1 +--- + +# Phase 2: Implement SJC price client with API key management + +## Overview + +Create `internal/modules/gold/vnappmob_client.go` with a client that refreshes, caches, and uses the VNAppMob API key, and exposes a method that returns a VND/lượng price. + +## Requirements + +- Self-contained client: refresh key on demand, cache in KV, parse expiry from JWT payload. +- Env overrides for base URL and token. +- 403 from SJC endpoint triggers one refresh retry. +- No new external dependencies beyond standard library. + +## Architecture + +```go +type VNAppMobClient struct { + HTTP *http.Client + BaseURL string // default https://api.vnappmob.com + Token string // optional env override GOLD_VNAPP_API_KEY + KV storage.KVStore // module KV + nowFn func() time.Time +} + +func NewVNAppMobClientFromEnv(kv storage.KVStore) *VNAppMobClient + +func (c *VNAppMobClient) FetchSJCPrice(ctx context.Context) (buy, sell float64, err error) +``` + +- Key is stored under `"vnappmob:api_key"`. +- Helper `getKey(ctx)` returns the current valid key, refreshing if needed. +- Helper `refreshKey(ctx)` calls `POST {BaseURL}/api/request_api_key?scope=gold`, validates JWT, stores JSON. +- Helper `jwtExp(token)` extracts middle segment, base64-url decodes, JSON-parses `exp`. +- Helper `isExpired(token)` returns true if `exp - now < 24h` or parse fails. + +## Related Code Files + +- Create: `internal/modules/gold/vnappmob_client.go` +- Reference: `internal/modules/gold/price_urls.go` +- Reference: `internal/modules/stock/income_events.go` + +## Implementation Steps + +1. Add constants: + ```go + vnappmobDefaultURL = "https://api.vnappmob.com" + vnappmobKeyCacheKey = "vnappmob:api_key" + vnappmobRefreshBuffer = 24 * time.Hour + vnappmobHTTPTimeout = 10 * time.Second + ``` +2. Implement `NewVNAppMobClientFromEnv(kv)` reading `GOLD_VNAPP_API_URL` and `GOLD_VNAPP_API_KEY`. +3. Implement `getKey`/`refreshKey`/`jwtExp`/`isExpired`. +4. Implement `FetchSJCPrice`: build request, attach Bearer token, decode `{"results":[...]}`, validate first result, return buy/sell. +5. On 403, call `refreshKey` once and retry. + +## Success Criteria + +- [x] `VNAppMobClient` compiles and passes go vet. +- [x] Unit tests for JWT expiry parsing cover valid, malformed, and missing `exp`. +- [x] Mock server test verifies 403 triggers refresh and retry. +- [x] `FetchSJCPrice` returns `ErrNoGoldPrice` when response has no results or invalid values. + +## Risk Assessment + +- **Risk**: KV not available in local `memory` provider. **Mitigation**: KVStore is always passed; memory provider implements the interface. +- **Risk**: Race between concurrent refreshes. **Mitigation**: simple last-write-wins is acceptable; key validity is the same for all callers. diff --git a/plans/phase-03-wire-client-into-gold-module-and-handlers.md b/plans/phase-03-wire-client-into-gold-module-and-handlers.md new file mode 100644 index 0000000..7c8ea9a --- /dev/null +++ b/plans/phase-03-wire-client-into-gold-module-and-handlers.md @@ -0,0 +1,99 @@ +--- +phase: 3 +title: "Wire client into gold module and handlers" +status: completed +priority: P1 +effort: "2h" +dependencies: [2] +--- + +# Phase 3: Wire client into gold module and handlers + +## Overview + +Integrate the VNAppMob SJC client as the primary gold price provider while keeping the existing XAU/USD chain as fallback. Update the `priceFetcher` interface and handlers so `/gold_price` shows SJC data and trading commands use SJC-derived prices. + +## Requirements + +- VNAppMob SJC is the first price source. +- On VNAppMob failure, fall back to existing `GoldPriceClient.FetchLuongPrice`. +- `/gold_price` output shows SJC buy/sell in VND/lượng. +- Portfolio commands (`gold_buy`, `gold_sell`, `gold_stats`) use a representative price (e.g. mid of buy/sell or sell price). + +## Architecture + +Introduce a composite fetcher in `internal/modules/gold/prices.go`: + +```go +type compositePriceFetcher struct { + vnappmob *VNAppMobClient + fallback *GoldPriceClient +} + +func (f *compositePriceFetcher) FetchLuongPrice(ctx context.Context) (float64, error) { + buy, sell, err := f.vnappmob.FetchSJCPrice(ctx) + if err == nil { + return (buy + sell) / 2, nil + } + log.Warn("vnappmob_sjc_failed", "err", err) + return f.fallback.FetchLuongPrice(ctx) +} + +func (f *compositePriceFetcher) FetchPrice(ctx context.Context) (GoldPrice, error) { + buy, sell, err := f.vnappmob.FetchSJCPrice(ctx) + if err == nil { + mid := (buy + sell) / 2 + return GoldPrice{XAUUSD: 0, USDVND: 0, VNDPerLuong: mid}, nil + } + return f.fallback.FetchPrice(ctx) +} +``` + +Update `newState(kv)` to build the composite fetcher. `helpers.go` already defines the `priceFetcher` interface. + +Update `handlePrice` in `handlers.go`: +- If `FetchPrice` returned from SJC (detect via new flag or by checking `XAUUSD == 0`), show SJC-specific output. +- Otherwise keep existing spot-price output for fallback. + +Add a field to `GoldPrice` to indicate the source, e.g.: +```go +type GoldPrice struct { + XAUUSD float64 + USDVND float64 + VNDPerLuong float64 + Source string // "vnappmob-sjc" or "xau-fallback" + SJC *SJCPrice // optional +} + +type SJCPrice struct { + Buy float64 + Sell float64 +} +``` + +## Related Code Files + +- Modify: `internal/modules/gold/prices.go` +- Modify: `internal/modules/gold/helpers.go` +- Modify: `internal/modules/gold/handlers.go` +- Reference: `internal/modules/gold/gold.go` + +## Implementation Steps + +1. Extend `GoldPrice` struct with `Source string` and `SJC *SJCPrice`. +2. Implement `compositePriceFetcher` in `prices.go` (or new `composite_prices.go`). +3. Change `newState(kv)` to use composite fetcher. +4. Update `handlePrice` to render SJC-specific lines when `Source == "vnappmob-sjc"`. +5. Ensure `handleBuy`, `handleSell`, `handleStats` continue to work via `FetchLuongPrice`. + +## Success Criteria + +- [x] `/gold_price` shows SJC buy/sell when VNAppMob succeeds. +- [x] `/gold_price` falls back to old output when VNAppMob fails. +- [x] `gold_buy` and `gold_sell` use SJC mid price for cost/revenue. +- [x] `go vet` passes. + +## Risk Assessment + +- **Risk**: SJC response has only one of `buy_1l`/`sell_1l`. **Mitigation**: if one is missing, use the other; if both missing, return error so fallback kicks in. +- **Risk**: Existing tests assume `GoldPrice` shape. **Mitigation**: add fields without removing old ones. diff --git a/plans/phase-04-update-iac-and-env-handling.md b/plans/phase-04-update-iac-and-env-handling.md new file mode 100644 index 0000000..b94640b --- /dev/null +++ b/plans/phase-04-update-iac-and-env-handling.md @@ -0,0 +1,55 @@ +--- +phase: 4 +title: "Update IaC and env handling" +status: completed +priority: P2 +effort: "1h" +dependencies: [2] +--- + +# Phase 4: Update IaC and env handling + +## Overview + +Add optional env vars for VNAppMob configuration, export them in `cmd/server/main.go`, and expose CloudFormation parameters in `template.yaml`. + +## Requirements + +- Allow manual API key override (`GOLD_VNAPP_API_KEY`). +- Allow base URL override (`GOLD_VNAPP_API_URL`) for testing. +- Support SSM Parameter Store injection via `GOLD_VNAPP_API_KEY_PARAMETER_NAME`. + +## Architecture + +In `cmd/server/main.go`: +- Add fields to `config`: `GoldVNAppAPIURL`, `GoldVNAppAPIKey`, `GoldVNAppAPIKeyParam`. +- Read env vars `GOLD_VNAPP_API_URL`, `GOLD_VNAPP_API_KEY`, `GOLD_VNAPP_API_KEY_PARAMETER_NAME`. +- Add binding to `resolveSSMSecrets`. +- Export via `exportOptionalEnv("GOLD_VNAPP_API_URL", ...)` and `GOLD_VNAPP_API_KEY`. + +In `template.yaml`: +- Add parameters `GoldVNAppAPIURL`, `GoldVNAppAPIKeyParameterName`. +- Add env vars under `BotFunction.Environment.Variables`. +- IAM policy already allows SSM fetch for `/miti99bot/${StackEnv}/*`, so no new policy needed. + +## Related Code Files + +- Modify: `cmd/server/main.go` +- Modify: `template.yaml` + +## Implementation Steps + +1. Extend `config` struct and `loadConfig`. +2. Add SSM binding and optional env export. +3. Add CFN parameters and pass to Lambda env. +4. Verify SSM path pattern matches existing IAM wildcard. + +## Success Criteria + +- [x] `GOLD_VNAPP_API_KEY` env var reaches the gold module. +- [x] `GOLD_VNAPP_API_KEY_PARAMETER_NAME` is fetched from SSM at startup. +- [x] `template.yaml` deploys without syntax errors (`sam validate`). + +## Risk Assessment + +- **Risk**: Forgetting to export env var means `os.Getenv` in module sees nothing. **Mitigation**: mirror existing `exportOptionalEnv` calls exactly. diff --git a/plans/phase-05-tests-and-verification.md b/plans/phase-05-tests-and-verification.md new file mode 100644 index 0000000..8d577ec --- /dev/null +++ b/plans/phase-05-tests-and-verification.md @@ -0,0 +1,56 @@ +--- +phase: 5 +title: "Tests and verification" +status: completed +priority: P1 +effort: "2h" +dependencies: [3, 4] +--- + +# Phase 5: Tests and verification + +## Overview + +Add unit tests for the new client and integration smoke tests for the composite fetcher, then run the full suite. + +## Requirements + +- Test VNAppMob key refresh, storage, expiry parsing, 403 retry, and SJC price parsing. +- Test composite fetcher fallback behavior. +- Ensure existing gold module tests still pass. + +## Architecture + +Create `internal/modules/gold/vnappmob_client_test.go`: +- `TestJWTExp` — valid, expired, malformed tokens. +- `TestRefreshKey` — mock refresh endpoint, verify KV storage. +- `TestFetchSJCPrice` — mock SJC endpoint, verify buy/sell. +- `TestFetchSJCPrice_403Refreshes` — first 403, refresh, second 200. +- `TestFetchSJCPrice_FallbackError` — on total failure, return `ErrNoGoldPrice`. + +Create/update `internal/modules/gold/prices_test.go` or `composite_prices_test.go`: +- `TestCompositeFetcher_PrefersVNAppMob` +- `TestCompositeFetcher_FallsBack` + +## Related Code Files + +- Create: `internal/modules/gold/vnappmob_client_test.go` +- Modify: existing test files in `internal/modules/gold/` + +## Implementation Steps + +1. Write `vnappmob_client_test.go` using `httptest.Server`. +2. Add composite fetcher tests with stub `priceFetcher` implementations. +3. Run `make vet` and `make test`. +4. Run local server with `MODULES=gold` and hit `/gold_price` via Telegram or curl if possible. + +## Success Criteria + +- [x] All new tests pass. +- [x] `make test` passes. +- [x] `make vet` passes. +- [ ] Manual local smoke test returns SJC price. + +## Risk Assessment + +- **Risk**: External API tests are flaky. **Mitigation**: use `httptest` for all unit tests; external calls only in manual smoke test. diff --git a/plans/plan.md b/plans/plan.md new file mode 100644 index 0000000..a3c0605 --- /dev/null +++ b/plans/plan.md @@ -0,0 +1,56 @@ +--- +title: Integrate VNAppMob SJC gold price with auto-refresh API key +description: >- + Add a VNAppMob SJC price provider to the gold module. The provider + self-manages a free JWT API key by refreshing it when missing or expired, + stores it in KV, and uses it to call the SJC endpoint. +status: completed +priority: P2 +branch: main +tags: + - gold + - vnappmob + - sjc + - api-key + - kv +blockedBy: [] +blocks: [] +created: '2026-06-15T02:15:10.444Z' +createdBy: 'ck:plan' +source: skill +--- + +# Integrate VNAppMob SJC gold price with auto-refresh API key + +## Overview + +Replace/add the gold spot-price source with VNAppMob's Vietnam SJC price feed (`api.vnappmob.com/api/v2/gold/sjc`). The feed returns VND/lượng directly, removing the XAU/USD + FX conversion step. It requires a free `api_key` JWT that expires in ~14 days, so the implementation must refresh and persist the key automatically. + +The existing `GoldPriceClient` provider chain is extended: VNAppMob SJC becomes the new primary provider; the old XAU/USD chain becomes the fallback. A new `VNAppMobClient` handles key refresh via `POST /api/request_api_key?scope=gold`, stores the key under KV (`vnappmob:api_key`), and uses it as `Authorization: Bearer ` for `GET /api/v2/gold/sjc`. + +## Phases + +| Phase | Name | Status | +|-------|------|--------| +| 1 | [VNAppMob API research and key refresh design](./phase-01-vnappmob-api-research-and-key-refresh-design.md) | Completed | +| 2 | [Implement SJC price client with API key management](./phase-02-implement-sjc-price-client-with-api-key-management.md) | Completed | +| 3 | [Wire client into gold module and handlers](./phase-03-wire-client-into-gold-module-and-handlers.md) | Completed | +| 4 | [Update IaC and env handling](./phase-04-update-iac-and-env-handling.md) | Completed | +| 5 | [Tests and verification](./phase-05-tests-and-verification.md) | Completed | + +## Dependencies + +- No blocking plans. This touches only `internal/modules/gold`, `cmd/server/main.go`, and `template.yaml`. + +## Risks + +- VNAppMob key refresh endpoint may change or rate-limit. Mitigation: env override + fallback to existing XAU/USD chain. +- JWT parsing for expiry must not require a JWT library if possible (base64 + JSON). Mitigation: implement minimal JWT claim extraction; treat parse failure as "refresh needed". +- Concurrent Lambda containers may race to refresh the key. Mitigation: CAS-based single-flight or last-write-wins with short TTL. + +## Success Criteria + +- `/gold_price` returns SJC buy/sell prices in VND/lượng when VNAppMob is healthy. +- Missing/expired key triggers a refresh transparently without user error. +- If VNAppMob fails, the bot falls back to the existing XAU/USD-derived price. +- Env var `GOLD_VNAPP_API_KEY` can bypass auto-fetch for local dev or SSM injection. diff --git a/plans/reports/pm-260615-0948-vnappmob-gold-price-completion.md b/plans/reports/pm-260615-0948-vnappmob-gold-price-completion.md new file mode 100644 index 0000000..e17f93a --- /dev/null +++ b/plans/reports/pm-260615-0948-vnappmob-gold-price-completion.md @@ -0,0 +1,41 @@ +# VNAppMob SJC Gold Price Integration — Completion Report + +Date: 2026-06-15 +Plan: [Integrate VNAppMob SJC gold price with auto-refresh API key](../plan.md) + +## Status + +| Phase | Title | Status | +|-------|-------|--------| +| 1 | VNAppMob API research and key refresh design | Completed | +| 2 | Implement SJC price client with API key management | Completed | +| 3 | Wire client into gold module and handlers | Completed | +| 4 | Update IaC and env handling | Completed | +| 5 | Tests and verification | Completed (manual smoke test pending) | + +## Delivered + +- `internal/modules/gold/vnappmob_client.go` — client that auto-refreshes and caches the free VNAppMob JWT key in KV. +- `internal/modules/gold/composite_prices.go` — primary VNAppMob SJC fetcher with XAU/USD fallback. +- `internal/modules/gold/vnappmob_client_test.go` — JWT parsing, refresh, 401/403 retry, invalid-value tests. +- `internal/modules/gold/composite_prices_test.go` — preference and fallback behavior tests. +- Updated `internal/modules/gold/prices.go`, `helpers.go`, `handlers.go` for SJC output and composite wiring. +- Updated `cmd/server/main.go` with `GOLD_VNAPP_API_URL`, `GOLD_VNAPP_API_KEY`, and `GOLD_VNAPP_API_KEY_PARAMETER_NAME` config/SSM support. +- Updated `template.yaml` with new parameters and Lambda env vars. + +## Verification + +- `go test -race -count=1 ./...` — pass +- `go vet ./...` — clean +- `golangci-lint run ./...` — 0 issues +- `internal/modules/gold` coverage — 85.6% + +## Known Limitations / Notes + +- Manual live smoke test against `api.vnappmob.com` not performed. +- Cross-Lambda-container key refresh races are last-write-wins (acceptable per plan). +- 401 and 403 both trigger a single key refresh retry. + +## Unresolved Questions + +- Does VNAppMob return 401, 403, or both for rejected keys? Code handles both.