mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-11 03:13:46 +00:00
chore(plans): add VNAppMob integration plan and completion report
This commit is contained in:
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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user