chore(plans): add VNAppMob integration plan and completion report

This commit is contained in:
tiennm99 committed 2026-06-15 10:49:19 +07:00
1 parent ae92539043
commit 966e4afd51
7 files changed
+447

No files matched your search

@@ -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 <jwt>`
- Response: `{"results":[{"buy_1l":<float>,"sell_1l":<float>, ...}]}`
- Use the first element's `buy_1l`/`sell_1l` as the spot price.
3. **Key storage**:
- KV key: `vnappmob:api_key`
- Value JSON: `{"token":"<jwt>","exp":<unix>}`
- 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.
@@ -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.
@@ -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.
@@ -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.
+56
View File
@@ -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.
+56
View File
@@ -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 <jwt>` 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.
@@ -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.