docs: remove completed implementation records

This commit is contained in:
2026-07-21 19:13:34 +07:00
parent 9849fcec87
commit a964fe6d96
24 changed files with 0 additions and 1743 deletions
@@ -1,31 +0,0 @@
# Wheelofnames Command Journal
## Context
Executed plan `plans/260703-0404-wheelofnames-misc-command/plan.md`.
## What Changed
- Added public `/wheelofnames` command to `misc`.
- Command parses comma-separated options, trims whitespace, ignores empty entries, and returns one random valid option.
- Updated command registration tests and handler tests.
- Updated `telegram-commands.json` and README command list.
- Synced plan phases to completed.
## Decisions
- Plain text reply only; no HTML parse mode needed.
- Non-cryptographic randomness acceptable for casual choice selection.
- Duplicate options preserved, allowing intentional weighting.
- No stats migration needed because command is additive.
## Validation
- `go test ./internal/modules/misc`
- `go test ./cmd/server ./internal/modules/misc`
- `go test ./...`
- `go vet ./...`
## Unresolved Questions
None.
@@ -1,34 +0,0 @@
# Stock Dividend Command Naming Journal
## Context
Researched official VSDC dividend notices and brainstormed clearer stock command
contracts for cash, share, and mixed dividend events.
## What Happened
- Approved `/stock_cash_dividend <vnd_per_share> <TICKER>` for cash dividends.
- Approved `/stock_share_dividend <owned:new> <TICKER>` for share dividends.
- Approved `/stock_dividend <vnd_per_share> <owned:new> <TICKER>` for mixed
cash-and-share dividends.
- Official examples support ratios such as `4:1` and `100:10`.
- No implementation changes made yet.
## Decisions
- Share ratios use `owned:new`; both parts must be positive whole numbers.
- New shares use integer floor division. Fractional entitlements discarded.
- Cash input means VND paid per pre-event share.
- Mixed dividends calculate cash and new shares from the same pre-event holding,
then persist both results atomically.
- Stats history needs a one-time, idempotent migration: existing
`/stock_dividend` usage becomes `/stock_cash_dividend`, and `/stock_bonus`
usage becomes `/stock_share_dividend`. The new mixed `/stock_dividend` starts
with fresh stats.
## Next Steps
- Create an implementation plan covering handlers, registration, user-facing
text, command menu, atomic storage behavior, stats migrations, and tests.
- Implement the approved zero-result behavior: reject share-only payouts, but
still credit cash and report zero new shares for mixed payouts.
@@ -1,33 +0,0 @@
# Stock Dividend Command Plan Journal
## Context
Created the implementation plan at
`plans/260720-1616-stock-dividend-commands/` from the approved research and
brainstorm decisions.
## What Happened
- Added three pending phases: implement ratio dividend commands, migrate command
statistics, and verify user-facing contracts.
- Planned `/stock_cash_dividend` with VND/share, `/stock_share_dividend` with an
`owned:new` ratio, and combined `/stock_dividend` using both inputs.
- Preserved floor rounding, pre-event holdings, and one atomic portfolio save.
- Defined zero-share behavior: reject share-only events; combined events still
credit cash.
- Tightened migration design after review: each stats row gets a `prepared`
checkpoint containing its exact final target count. Crash retries set that
count instead of incrementing again.
- Could not persist the active-plan selection because `CK_SESSION_ID` was unset.
## Status
- Plan: pending
- Progress: 0/3 phases complete
- Source implementation: not started
## Next Steps
- Execute the three planned phases, beginning with command contracts and tests.
- Keep the stats migration before module registration so the reused
`/stock_dividend` name cannot misattribute historical usage.
@@ -1,31 +0,0 @@
# Stock Dividend Plan Validation Journal
## Context
Validated `plans/260720-1616-stock-dividend-commands/plan.md` and its three
pending phase documents before implementation.
## Validation Result
- Standard verification: 30 checked, 30 verified, 0 failed, 0 unverified.
- Confirmed cash input is a positive whole VND amount per share.
- Confirmed any positive whole-number `owned:new` ratio is accepted and echoed
unreduced.
- Confirmed repeated manual calls are allowed. No event ledger or duplicate-call
restriction; caller owns correctness.
- Confirmed permanent global and per-row migration markers. Incomplete runs
resume from `prepared` checkpoints using their exact target counts.
- Propagated these decisions through all three phase documents.
- Whole-plan consistency sweep reconciled one stale float-acceptance reference
with integer-input wording and found zero contradictions.
## Status
- Plan validation: complete
- Plan implementation: not started
- Source changes: none
## Next Steps
- Execute the validated phases in order: command behavior, stats migration, then
user-contract and full-project verification.
@@ -1,67 +0,0 @@
# Stock Dividend Commands Journal
## Context
Implemented the validated stock-dividend plan: clearer manual commands,
ratio-based share entitlement, and historical stats preservation.
## What Changed
- Added `/stock_cash_dividend <vnd_per_share> <TICKER>` for positive whole-VND
cash credits.
- Added `/stock_share_dividend <owned:new> <TICKER>` with unreduced ratio
preservation and floor-rounded whole shares.
- Changed `/stock_dividend <vnd_per_share> <owned:new> <TICKER>` to apply cash
and shares from the same pre-event holding with one portfolio save.
- Share-only zero entitlement rejects with the minimum holding; combined events
still credit cash when shares round to zero.
- Updated registration, command menu, usage text, README, and contract tests;
removed `/stock_bonus` from active commands.
## Stats Migration
- Migrates `stock_dividend -> stock_cash_dividend` and
`stock_bonus -> stock_share_dividend`, including anonymous and per-user rows.
- Merges existing target counts without loss.
- Permanent global and per-row markers retain migration history.
- Each row stores a `prepared` exact target count before mutation. Retries set
that count after any write-boundary failure, preventing double increments.
## Review Findings
- Guarded `strconv.ParseInt` range errors so its saturated return value cannot be
accepted as valid input.
- Added exact `int64` share formatting so quantities above `2^53` do not lose
digits through `float64` conversion.
- Enforced exact float-backed VND balance addition at the `2^53` boundary;
inexact sums reject without saving.
- Reviewer: 9/10, approve. Adversarial review: PASS; no disproven claims or
reachable regressions.
## Decisions
- Ratios remain positive integer `owned:new` values and are echoed as entered;
no normalization.
- Commands are manual adjustments. Repeated calls remain allowed, with no event
ledger or duplicate-call guard; caller owns correctness.
- Overflow, exactness, ticker, and syntax validation remain mandatory despite
the manual workflow.
## Verification
- Passed: `go test ./internal/modules/stock`
- Passed: `go test ./internal/modules/stats`
- Passed: `go test ./cmd/server`
- Passed: `go test ./...`
- Passed: `go vet ./...`
- Passed: `telegram-commands.json` PowerShell `ConvertFrom-Json`
- Passed: `git diff --check` (line-ending warnings only)
- Skipped: Mongo-backed tests because `MONGODB_TEST_URL` was unset.
- Skipped: `golangci-lint run` because the binary was not installed.
## Next Considerations
- Run Mongo-backed migration tests when a test database is available.
- Run the lint gate when `golangci-lint` is installed.
- After deployment, verify migrated counts and retained system markers before
considering migration-runtime cleanup; keep historical stats and markers.
@@ -1,47 +0,0 @@
# Cross-Platform Go Workflow Journal
## Context
Removed Unix-oriented Make targets so Windows, macOS, and Linux contributors
can use the same standard Go workflow with platform-specific environment syntax.
## What Changed
- Deleted `Makefile` and replaced its development shortcuts with documented
`go test`, `go vet`, `go build`, `go run`, and direct Docker commands.
- Documented Telegram webhook inspection and cleanup with PowerShell
`Invoke-RestMethod` and POSIX `curl` examples.
- Deleted `telegram-commands.json`; the runtime module registry already builds
and registers the command menu on every startup, so the JSON duplicated the
authoritative Go definitions.
- Replaced Makefile linker flags with Go's embedded VCS build metadata and kept
the existing seven-character commit SHA behavior for deploy notifications.
- Updated CI and MongoDB test guidance to point contributors to the portable
README workflow.
## Reflection
Removing the task wrapper makes the repository less convenient for habitual
`make` users, but avoids maintaining shell-specific orchestration and duplicate
command-menu data. Standard Go, Docker, and HTTP tools keep each operation
explicit and work across supported development platforms.
## Decisions
- The Go module registry is the single source of truth for Telegram commands.
- Local binaries obtain their short revision from Go build information; the
deployment-provided `SOURCE_COMMIT` remains preferred at runtime.
- Platform differences are documented only where shell syntax or HTTP tooling
differs.
## Verification
- Passed: `go test ./...`
- Passed: `go vet ./...`
- Passed: `go build ./...`
- Unavailable: `golangci-lint run` because the binary is not installed.
## Next Steps
- Run the lint gate when `golangci-lint` is available.
- Keep command registration tests aligned with future public command changes.
@@ -1,51 +0,0 @@
# Testcontainers MongoDB Integration Journal
## Context
MongoDB integration tests previously depended on contributors manually starting
a database and setting `MONGODB_TEST_URL`. The suite now provisions its own
MongoDB 8 instances when Docker is available.
## What Changed
- Added Testcontainers Go and MongoDB module v0.43.0.
- Added a shared lazy test manager that starts one `mongo:8` container per
`storage`, `lol`, and `stats` package and terminates it after that package's
suite.
- Preserved `MONGODB_TEST_URL` as an external-database override.
- Docker-unavailable environments emit an explicit warning and skip MongoDB
tests; a regression test verifies both the warning and skip behavior.
- Once the Docker provider is healthy, container startup and connection-string
failures are fatal. Cleanup failures also make an otherwise passing suite
fail.
- Updated README and CI guidance so normal Go test commands exercise
Testcontainers automatically.
## Reflection
Package-scoped lazy containers balance isolation and startup cost: packages do
not share database processes, while tests inside a package avoid repeatedly
starting MongoDB. Distinguishing an absent Docker daemon from a broken healthy
provider keeps local no-Docker runs usable without hiding real infrastructure
regressions.
## Decisions
- Pin tests and local setup guidance to MongoDB 8.
- Keep explicit external MongoDB support for constrained or pre-provisioned
environments.
- Treat lifecycle failures as test failures whenever Docker is available.
## Verification
- Passed: `go test ./...`
- Passed: `go vet ./...`
- Passed: `go build ./...`
- Passed: `golangci-lint run`
- Passed: `go mod tidy` with no residual module diff.
- Three real MongoDB containers ran, one for each integration-test package, and
all three were cleaned up successfully.
## Next Steps
- Monitor CI duration and container startup reliability after rollout.
@@ -1,64 +0,0 @@
# Telegram Command Discovery Journal
> Historical note: the later command-parameter convention simplifies
> `<options(comma-separated)>` to `<option,...>`, keeping the literal delimiter
> visible without prose inside the placeholder. See
> `docs/command-parameter-conventions.md`.
## Context
Telegram's native command menu and `/help` exposed only short descriptions,
leaving users to discover parameters through failed invocations or source
documentation.
## What Changed
- Extended the shared command registration with `Parameters` metadata and
presentation helpers used by both discovery surfaces.
- Added metadata for all 40 public commands, including the exact stats grammar:
`[users | user <username> | cmd <command_name>]`.
- Normalized placeholders to lowercase descriptive names, including meaningful
units or currencies; `[...]` marks optional input, `...` remaining free text,
and parentheses structured input.
- Normalized `/wheelofnames` to `<options(comma-separated)>` across metadata,
usage text, and tests.
- Finalized dividend placeholders as `<vnd_per_share> <ticker>`,
`<ratio(owned:new)> <ticker>`, and
`<vnd_per_share> <ratio(owned:new)> <ticker>` for cash, share, and combined
commands. Parsing remains unchanged.
- Native menu descriptions show parameters followed by the summary, while
`/help` renders the complete invocation followed by the summary. Neither
discovery surface includes example invocations. Dynamic fields are
HTML-escaped in `/help`.
- Registration validation rejects multiline metadata and public descriptions
over Telegram's 256-character limit.
- Updated user and deployment documentation for the shared registry behavior.
## Reflection
Keeping syntax beside each handler registration prevents the native menu,
`/help`, and implementation from drifting independently. Both discovery
surfaces stay compact without sacrificing safe HTML rendering in `/help`.
## Decisions
- Existing command names, handlers, parsers, and persisted data remain
unchanged; normalization is presentation-only.
- No command was added, renamed, or deleted, so no stats migration is needed.
- Discovery surfaces intentionally omit example invocations; handler usage
errors may still include focused examples.
- The complete `/help` output remains within Telegram's 4,096-character limit.
## Verification
- Passed: command presentation, validation, menu, and `/help` tests for all 40
public commands.
- Passed: `go test ./...`, including real MongoDB Testcontainers suites.
- Passed: `go vet ./...`
- Passed: `go build ./...`
- Passed: `golangci-lint run`
## Next Steps
- Require parameter metadata updates alongside future public command contract
changes.
@@ -1,44 +0,0 @@
# Command Parameter Conventions Journal
## Context
Command parameter labels had grown organically, especially for comma-separated
arguments. Researched common CLI notation to establish a minimal display syntax
for Telegram's native menu, `/help`, and handler usage text.
## What Changed
- Added the evergreen `docs/command-parameter-conventions.md` reference with
concise rules for required, optional, variadic, alternative, and structured
parameters.
- Recorded the user-selected `<option,...>` notation for required
comma-separated values.
- Updated `/random` and `/wheelofnames` metadata, usage text, and contract tests
from `<options(comma-separated)>` to `<option,...>`.
- Linked project guidance and README command-discovery documentation to the
shared convention reference.
## Reflection
The compact notation communicates both one-or-more values and the literal comma
separator without turning display metadata into a schema language. Keeping the
rules in an evergreen document avoids repeating policy in project instructions
and feature docs.
## Decisions
- Parameter strings remain presentation-only; handlers own validation.
- `/random` and `/wheelofnames` parsing and runtime behavior remain unchanged.
- Metadata, usage errors, and tests must use the same exact notation.
## Verification
- Passed: focused command-presentation and misc tests.
- Passed: `go test ./...`
- Passed: `go vet ./...`
- Passed: `go build ./...`
- Passed: `golangci-lint run`
## Next Steps
- Apply the evergreen conventions whenever command parameters change.
@@ -1,59 +0,0 @@
# Stock and Coin Cost Basis P&L Journal
## Context
Stock and coin portfolios tracked account funding and holdings but not the
remaining acquisition cost of each position, so they could not distinguish
realized sale results from unrealized open-position performance.
## What Changed
- Added persisted per-symbol total remaining `costBasis` to stock and coin
portfolios, with invariant checks on load, update, and save paths.
- Buys add actual spend; partial sells remove proportional weighted-average
basis and report realized P&L; full sells remove the position and its basis.
- Portfolio views now show average entry price and per-position unrealized P&L.
Account P&L remains total account value minus top-ups and is withheld when
missing quotes make valuation partial.
- Stock share dividends preserve total basis while increasing quantity; cash
dividends and top-ups remain outside position basis.
- Sorted, bounded portfolio replies retain summaries while omitting excess
position lines before Telegram's message limit.
## Migration Safety
- Enabled modules scan every portfolio before handlers are installed on every
boot, even when a completion marker already exists.
- Missing legacy basis is initialized from a complete current quote set, giving
each migrated position zero initial unrealized P&L without repricing rows that
already have basis.
- Versioned writes retry conflicts; the shared `system` marker is written only
after all rows succeed and remains an audit record rather than a scan bypass.
- A two-minute overall deadline bounds startup. Invalid data, noncanonical
symbols, missing quotes, exhausted conflicts, or storage failures abort
startup instead of allowing trades with unknown basis.
## Decisions
- Persist total remaining cost, deriving average entry price as basis divided by
held quantity; do not persist trade lots or cumulative realized P&L.
- Migrate only loaded modules, so disabled-module data waits until re-enabled.
- Preserve all command names, parameter contracts, and existing portfolio data.
## Verification
- Passed: focused stock and coin accounting, migration, output, and reply-budget
tests.
- Passed: MongoDB 8 Testcontainers migration and idempotency coverage.
- Passed: `go test ./...`
- Passed: `go vet ./...`
- Passed: `go build ./...`
- Passed: `golangci-lint run`
## Operational Impact
- Deployments with legacy holdings may perform market-price lookups and writes
during startup; a required provider or database failure intentionally keeps
the bot offline until initialization can complete safely.
- Subsequent healthy boots still verify invariants but do not reprice completed
positions.
@@ -1,87 +0,0 @@
---
type: technical-journal
topic: dividend-api-and-migration-cleanup
conducted_at: 2026-07-21T17:04:00+07:00
status: complete
---
# Dividend API and Migration Cleanup Journal
## Context
Portfolio schema migrations were verified complete in production, so their
compatibility code had become permanent startup and maintenance overhead. Coin
positions also inherited a stock-only dividend cursor with no valid use.
Separately, research was needed before designing automatic discovery of
Vietnamese stock dividend events.
## What Happened
- Removed `dividendCheckedAt` from coin positions, validation, buy behavior,
and tests. Stock retains the cursor because dividend events apply there.
- Retired completed stock and coin portfolio migrations plus the completed
stats command-renaming migration and their migration-only tests.
- Preserved recurring startup maintenance: stats query indexes and the LoL
match-cache TTL index remain idempotently initialized and tested.
- Preserved historical migration records in MongoDB's `system` collection and
kept the reusable `internal/systemstate` helper for future migrations.
- Removed legacy numeric-position JSON/BSON decoders after production schema
completion was confirmed. Current models now describe only the supported
nested asset schema.
- Accepted lazy cleanup of stale coin cursor fields. BSON decoding ignores the
old unknown field, and the next portfolio mutation uses versioned
`ReplaceOne`, rewriting that portfolio in the current shape without a new
one-time migration.
## Dividend API Research
SSI iBoard's corporate-actions endpoint is the recommended initial source. It
currently returns anonymous JSON with ticker/date filters, pagination, stable
`CorId` values, cash amounts, ratios, and event dates. It covered verified cash
and share dividend examples.
The endpoint is undocumented and has no published SLA, rate limit, or stability
contract. Any implementation should therefore isolate it behind a replaceable
provider, validate and locally classify events, deduplicate by `CorId`, and ask
the user to confirm before changing a portfolio. VSDC remains the authoritative
notice source; a licensed FiinGroup feed is the stronger future option if this
becomes production-critical.
## Decisions
- Keep dividend state stock-only; coin assets persist only `quantity` and
total remaining `base`.
- Remove completed one-time runtime code rather than continuing to scan already
migrated portfolios on every boot.
- Do not delete system history or general migration infrastructure.
- Do not introduce a cleanup migration solely for stale coin BSON fields;
normal writes remove them safely over time.
- Treat SSI iBoard as a replaceable prototype provider, not a guaranteed public
API contract.
## Verification
- Passed focused tests:
`go test -count=1 ./internal/modules/coin ./internal/modules/stock ./internal/modules/stats ./cmd/server`
- Passed full suite: `go test -count=1 ./...`
- MongoDB 8 tests executed and passed for stats indexes and the LoL TTL index.
- Passed: `go vet ./...`
- Passed: `go build ./...`
- Passed with zero issues: `golangci-lint run`
- Passed: `git diff --check` (expected LF/CRLF working-copy warnings only).
- Independent tester, debugger, and reviewer found no defects.
## Reflection
Migration code is operationally valuable only while incompatible data can
still exist. Removing it after verification narrows startup failure modes and
makes the active persistence contract explicit. Lazy removal is appropriate
for an ignored field because it does not affect reads or correctness; structural
schema changes still require guarded migrations.
## Next
Design the Telegram interaction for listing SSI dividend events, including date
windows, pagination, exact ratio conversion, ambiguity handling, and explicit
user confirmation. Preserve manual dividend commands and keep automatic event
application out of scope until that interaction is approved.
@@ -1,73 +0,0 @@
---
type: technical-journal
topic: stock-dividend-event-buttons
conducted_at: 2026-07-21T18:15:00+07:00
status: complete
---
# Stock Dividend Event Buttons Journal
## Context
`/stock_portfolio` needed to surface recent dividend events for held tickers
without obscuring the portfolio or silently changing financial state. SSI
iBoard was selected as a best-effort discovery source, while the existing
manual dividend commands remain available.
## What Happened
- The portfolio response is sent first. Each discovered cash or explicit share
dividend follows in its own message with an `Apply dividend` button; a ticker
with no relevant event produces no extra message.
- Added a replaceable SSI provider with pagination, local event classification,
stable event IDs, deduplication, and exact ratio handling.
- Added callback registration to the shared module framework and persisted
short-lived pending actions separately from portfolios.
- Successful checks advance `dividendCheckedAt`; failed checks leave the cursor
unchanged and never prevent the portfolio from being displayed.
- Added `openedAt` to distinguish the holding lifecycle and retained applied SSI
event identities for auditability and replay protection.
SSI discovery accepts only day-granularity date windows. Queries therefore use
a cursor overlap at the lower boundary, then locally filter and deduplicate the
returned events. This avoids missing events around a date boundary while stable
SSI event IDs prevent overlap from producing repeated actionable suggestions.
## Key Decisions
- Callback data carries an opaque random token, not trusted dividend values.
Pending actions expire after 24 hours and are bound to the requesting
Telegram user, originating chat, and event message.
- A callback reloads the current portfolio and calculates from the holding at
click time. Another group member, an expired action, or a holding sold and
reopened after suggestion creation cannot apply it.
- Applied SSI event IDs are recorded atomically with the portfolio mutation, so
duplicate clicks and retries cannot apply the same provider event twice.
- Event fetches occur outside the per-user mutation lock. Cursor persistence
reloads and merges under that lock, preserving concurrent buys, sells, and
manual dividends without holding a lock across network calls.
- Notification or provider failures do not advance the affected cursor. A later
portfolio request can safely retry rather than lose an event.
## Verification
- Focused stock and module tests passed.
- Full suite passed: `go test -count=1 ./...`.
- Stock race tests and 20-iteration concurrency stress tests passed.
- `go vet ./...` and `go build ./...` passed.
- `golangci-lint run` completed with zero issues.
- Independent testing, debugging, and code review found no blockers.
## Risks and Limitations
- SSI iBoard is undocumented and has no published stability or availability
guarantee; the provider can require replacement if its contract changes.
- The portfolio has no dated transaction lots, so current quantity is not proof
of record-date entitlement. Users must verify the issuer notice.
- Manual commands do not record an SSI event ID. Applying manually and later
accepting the related button can count the same dividend twice.
## Next
Observe SSI response stability and callback behavior in real use. Commit these
changes only if the user requests it.
@@ -1,109 +0,0 @@
---
phase: 1
title: Implement Ratio Dividend Commands
status: completed
effort: ''
priority: P1
dependencies: []
---
# Phase 1: Implement Ratio Dividend Commands
<!-- Updated: Validation Session 1 - manual-input policy and ratio/cash contracts -->
## Context Links
- [Approved brainstorm](../reports/260720-1612-stock-dividend-command-brainstorm.md)
- [VSDC ratio research](../reports/260720-1608-dividend-notice-ratio-research.md)
## Overview
Replace the two ambiguous adjustment handlers with cash-only, share-only, and
combined dividend contracts. Centralize validated ratio math so all handlers
use the same overflow-safe, floor-rounded calculation.
## Requirements
- Functional: register `stock_cash_dividend`, `stock_share_dividend`, and
`stock_dividend` with the approved argument order and clear usage examples.
- Functional: accept cash only as positive whole VND per share; reject signs,
decimals, zero, parse overflow, and non-finite representations.
- Functional: accept only positive whole-number `owned:new` parts; reject
missing/extra colons, decimals, signs, zero, parse overflow, and invalid ticker.
- Functional: accept equivalent unreduced ratios and preserve the user's exact
valid ratio text in the success reply; do not require or display reduction.
- Functional: compute `floor(held * new / owned)` without overflowing `int64`;
compute the minimum holding for a non-zero share result safely.
- Functional: share-only rejects a zero result and reports that minimum;
combined credits cash and reports zero shares.
- Non-functional: cash and shares use the same pre-event holding; mutate only
after all validation; call `SavePortfolio` exactly once per successful event.
- Non-functional: allow intentional repeated calls; add no event ID, notice
lookup, history ledger, or duplicate-event guard.
## Architecture
Add a small dividend calculation helper beside the stock handlers. Parse cash
and ratio parts into positive integers while retaining the validated ratio
string for replies. Calculate quotient and remainder before
multiplication (or use checked operations) to preserve floor semantics without
`held * new` overflow. Each handler loads once, snapshots `held`, calculates
all outputs, mutates the in-memory portfolio, then saves once. The combined
handler must not let newly issued shares participate in its cash calculation.
Treat every successful invocation as an intentional manual adjustment.
## Related Code Files
- Create: `C:\Users\miti99\Workspaces\tiennm99\miti99bot\internal\modules\stock\dividends.go` — ratio parsing and checked entitlement math.
- Create: `C:\Users\miti99\Workspaces\tiennm99\miti99bot\internal\modules\stock\dividends_test.go` — parser, floor, minimum, and overflow boundaries.
- Modify: `C:\Users\miti99\Workspaces\tiennm99\miti99bot\internal\modules\stock\handlers.go` — three handlers and user-facing replies.
- Modify: `C:\Users\miti99\Workspaces\tiennm99\miti99bot\internal\modules\stock\handlers_test.go` — contracts, failures, pre-event basis, and one-save behavior.
- Modify: `C:\Users\miti99\Workspaces\tiennm99\miti99bot\internal\modules\stock\stock.go` — registry names, descriptions, handlers.
- Modify: `C:\Users\miti99\Workspaces\tiennm99\miti99bot\internal\modules\stock\portfolio_test.go` — portfolio outcomes where helper coverage belongs.
## Implementation Steps
1. Write table tests first for whole-VND parsing, `4:1`, `100:10`, equivalent
unreduced ratios, malformed inputs, `int64` boundaries, exact division,
floor division, preserved ratio text, and safe minimum holdings.
2. Add handler tests for usage text, invalid cash/ratio/ticker, no holdings,
zero-result divergence, and persistence failures.
3. Add combined tests proving cash and shares derive from the same snapshot and
one store write commits both; use an instrumented test store to count saves.
4. Implement parsing and overflow-safe integer entitlement helpers. Require
positive whole VND/share and reject fractional or overflowing totals.
5. Replace `handleBonus`/old cash `handleDividend` routing with explicit cash,
share, and combined handlers. Replies include ratio, old holding, cash, new
shares, and final holding as applicable.
6. Update the command registry to eight stock commands and remove public
registration of `stock_bonus`.
7. Run `gofmt` and focused stock tests.
## Success Criteria
- [x] All three approved commands and examples are registered exactly.
- [x] Cash rejects fractional VND; valid unreduced ratios are accepted and echoed unchanged.
- [x] Share math floors without overflow for every accepted `int64` input.
- [x] Share-only zero entitlement makes no change; combined zero entitlement credits cash and reports zero shares.
- [x] Successful combined events use pre-event holdings and one `SavePortfolio`.
- [x] Repeating a valid command applies the adjustment again; no deduplication state exists.
- [x] `go test ./internal/modules/stock` passes.
## Risk Assessment
- Overflow or invalid numeric acceptance could silently over-credit portfolios.
Mitigate with integer parsing, quotient/remainder math, checked cash totals,
and boundary tests.
- Partial mutation could diverge balances. Validate first, mutate in memory,
persist once; verify the unchanged state on every rejected path.
- Manual repeated calls can double-credit an event by design. State caller
responsibility clearly; do not silently infer or suppress duplicates.
## Security Considerations
No new authorization surface. Continue sender checks and strict ticker/input
validation; do not echo unbounded raw input.
## Next Steps
Phase 2 migrates persisted stats before the renamed commands are deployed.
@@ -1,113 +0,0 @@
---
phase: 2
title: Migrate Command Statistics
status: completed
effort: ''
priority: P1
dependencies:
- 1
---
# Phase 2: Migrate Command Statistics
<!-- Updated: Validation Session 1 - prepared-checkpoint retry and retention -->
## Context Links
- [Approved compatibility decision](../reports/260720-1612-stock-dividend-command-brainstorm.md#compatibility-and-touchpoints)
- [Project stats compatibility rules](../../AGENTS.md#stats-compatibility)
## Overview
Move historical usage to the commands that retain the old meanings, then leave
`stock_dividend` empty for new combined-event usage. Guard the one-time move in
the shared `system` collection and run it during server startup.
## Requirements
- Functional: migrate `stock_dividend -> stock_cash_dividend` and
`stock_bonus -> stock_share_dividend`.
- Functional: include command-total rows (`uid=0`) and every per-user row;
merge source counts into existing target rows rather than overwrite them.
- Functional: preserve user ID/username metadata, remove migrated source rows,
and write the completion marker only after both mappings finish.
- Functional: a completed marker makes subsequent startups a no-op; tests cover
target merging and repeated invocation for memory and MongoDB stores.
- Functional: an incomplete migration resumes both remaining source rows and
`prepared` row checkpoints, including a checkpoint whose source was deleted.
- Functional: retain global and per-row markers permanently as migration history.
- Non-functional: migration errors fail startup before module registration, so
new command meanings never run against unmigrated history.
## Architecture
Extend stats startup maintenance to receive both `stats` and `system`
collections. Use the existing typed stats documents and `systemstate.Store`.
A stable marker such as `migration:stock-dividend-command-stats-v1` records
global completion. For each source row, first persist a stable per-row
`prepared` checkpoint in `system` containing the exact final target count.
Retries set the target to that checkpointed count instead of adding again,
then delete the source and mark the row complete. Preserve the best available
username deterministically. Mark global completion only after every row is
complete. On startup without a global completion marker, enumerate both source
rows and prepared checkpoints so deletion-before-row-complete can recover.
Retain all markers as migration history; add no cleanup path.
## Related Code Files
- Modify: `C:\Users\miti99\Workspaces\tiennm99\miti99bot\internal\modules\stats\startup.go` — startup signature, marker guard, and migration orchestration.
- Create: `C:\Users\miti99\Workspaces\tiennm99\miti99bot\internal\modules\stats\startup_test.go` — memory migration, merges, anonymous/users, failures, idempotency.
- Modify: `C:\Users\miti99\Workspaces\tiennm99\miti99bot\internal\modules\stats\startup_mongo_test.go` — Mongo indexes plus migration parity and rerun checks.
- Modify: `C:\Users\miti99\Workspaces\tiennm99\miti99bot\internal\modules\stats\stats_test.go` — visible stats attribution after migration if needed.
- Modify: `C:\Users\miti99\Workspaces\tiennm99\miti99bot\cmd\server\main.go` — pass `stats` and shared `system` collections before module build.
- Reference: `C:\Users\miti99\Workspaces\tiennm99\miti99bot\internal\systemstate\systemstate.go` — existing marker store; no schema change expected.
## Implementation Steps
1. Add memory tests seeding source-only, target-only, merged anonymous, merged
per-user, multiple users, and both command mappings.
2. Assert exact post-migration keys/counts/metadata, source removal, marker
content, and no change after a second startup call.
3. Add injected-failure tests at prepare, target-write, source-delete, and
row-complete boundaries. Retry each case—including source already deleted—
and prove counts never duplicate.
4. Implement deterministic global and per-row marker keys. A prepared marker
stores the exact merged target count; retries overwrite with that value,
never increment an already migrated target again.
5. Implement the mapping helper using deterministic `usageKey` values and the
existing typed store. Keep mapping data declarative and local to stats.
6. Extend `InitStore` to create indexes and run the guarded migration; wrap
errors with migration/command context.
7. Wire `provider.Collection(systemstate.CollectionName)` from server startup
and preserve fail-fast logging before command registration.
8. Mirror critical merge, partial-retry, and idempotency cases in Mongo tests.
9. Run `gofmt`, focused stats tests, and focused server tests.
## Success Criteria
- [x] Old cash usage appears only under `stock_cash_dividend`.
- [x] Old bonus usage appears only under `stock_share_dividend`.
- [x] Existing target counts merge for anonymous and per-user rows without loss.
- [x] Retries after every partial-write boundary cannot duplicate counts.
- [x] Prepared checkpoints resume even when their source rows no longer exist.
- [x] Completed migration is idempotent and marker-backed in memory and MongoDB.
- [x] Global and per-row migration markers remain after completion.
- [x] Startup aborts on migration failure and `go test ./internal/modules/stats ./cmd/server` passes.
## Risk Assessment
- Reusing `stock_dividend` before migration would mislabel history. Keep the
migration before module construction and treat errors as fatal.
- A mid-migration process crash may leave partial work. Prepare each row's exact
final count before touching the target; retry by setting that value, not by
adding again. Scan prepared checkpoints independently from source rows. Do
not mark row/global completion early or delete historical markers.
## Security Considerations
The migration touches counts and public usernames only. Never log full records;
log marker/mapping names and aggregate counts.
## Next Steps
Phase 3 aligns all user-facing surfaces and runs the repository-wide gate.
@@ -1,95 +0,0 @@
---
phase: 3
title: Verify User Contracts
status: completed
effort: ''
priority: P1
dependencies:
- 1
- 2
---
# Phase 3: Verify User Contracts
<!-- Updated: Validation Session 1 - document manual caller responsibility -->
## Context Links
- [Approved command contracts](../reports/260720-1612-stock-dividend-command-brainstorm.md#approved-behavior)
- [Repository development rules](../../AGENTS.md#command-changes)
## Overview
Synchronize Telegram command-menu metadata and README documentation with the
implemented contracts, then run focused and repository-wide verification.
## Requirements
- Functional: command names, descriptions, argument order, examples, errors,
and menu behavior agree across registry, handlers, JSON, tests, and README.
- Functional: remove `stock_bonus` from active user surfaces; document that
cash is VND/share and share input is the `owned:new` notice ratio.
- Functional: document positive whole-VND input, acceptance of unreduced ratios,
preserved ratio text, and deliberate repeat-call behavior.
- Non-functional: preserve plan-approved migration behavior and run every gate
required for command, storage, migration, and shared startup changes.
## Architecture
Treat registered Go commands as runtime truth and `telegram-commands.json` as
the manual BotFather/menu source. Tests assert the final eight-command stock
registry and registered menu contents. README gives concise user-visible syntax
and the floor/pre-event behavior needed to use notices correctly. It states
that commands are manual adjustments: the caller verifies the notice and avoids
accidental duplicates; the bot enforces syntax and storage safety only.
## Related Code Files
- Modify: `C:\Users\miti99\Workspaces\tiennm99\miti99bot\telegram-commands.json` — replace old dividend/bonus entries with three approved commands.
- Modify: `C:\Users\miti99\Workspaces\tiennm99\miti99bot\README.md` — document syntax, VND/share, ratio direction, floor rounding, and combined behavior.
- Modify: `C:\Users\miti99\Workspaces\tiennm99\miti99bot\internal\modules\stock\handlers_test.go` — final registry and user-facing text assertions.
- Modify: `C:\Users\miti99\Workspaces\tiennm99\miti99bot\cmd\server\command_menu_test.go` — command-menu presence and removal checks.
- Verify: `C:\Users\miti99\Workspaces\tiennm99\miti99bot\internal\modules\stats\startup_test.go` — migration contracts remain green.
## Implementation Steps
1. Replace `stock_bonus` and old cash-only `stock_dividend` menu descriptions
with `stock_cash_dividend`, `stock_share_dividend`, and combined
`stock_dividend`; keep Telegram ordering coherent.
2. Add a compact README stock-command section with all three syntaxes and one
mixed example. State positive whole VND/share, `owned:new`, accepted
unreduced ratios, preserved ratio replies, integer floor rounding,
pre-event basis, and caller responsibility for repeat events.
3. Update registry/menu tests to assert new commands and reject old public
`stock_bonus`; ensure descriptions stay within Telegram limits.
4. Run `gofmt` over every changed Go file.
5. Run focused packages: `go test ./internal/modules/stock`,
`go test ./internal/modules/stats`, and `go test ./cmd/server`.
6. Run `go test ./...` and `go vet ./...`.
7. If `golangci-lint` is available, run `golangci-lint run`; record a skipped
gate explicitly when the binary is absent.
## Success Criteria
- [x] Registry, handlers, JSON, README, and menu tests expose the same three contracts.
- [x] No active user-facing surface advertises `stock_bonus` or old cash-only `/stock_dividend` syntax.
- [x] README distinguishes manual correctness responsibility from mandatory parser/overflow safety.
- [x] Focused tests, `go test ./...`, and `go vet ./...` pass.
- [x] `golangci-lint run` passes when available.
- [x] No unresolved questions remain.
## Risk Assessment
- Stale menu/docs can cause financially wrong manual entries. Search all active
surfaces for old names and syntax, while leaving migration fixtures intact.
- Mongo integration tests may require local infrastructure. Keep unit coverage
mandatory and report environment-based integration skips accurately.
## Security Considerations
Documentation must use synthetic holdings and tickers only; no tokens,
production data, or private portfolio records.
## Next Steps
Implementation can proceed phase-by-phase after plan approval.
@@ -1,133 +0,0 @@
---
title: Stock Dividend Commands
description: >-
Replace ambiguous stock adjustments with cash-only, share-only, and combined
dividend commands while preserving historical stats.
status: completed
priority: P1
branch: main
tags:
- feature
- backend
- database
blockedBy: []
blocks: []
created: '2026-07-20T09:16:13.694Z'
createdBy: 'ck:plan'
source: skill
---
# Stock Dividend Commands
## Overview
Add three explicit public contracts: cash dividend in VND/share, share dividend
in `owned:new` ratio form, and a combined command. Use integer floor rounding,
pre-event holdings, and one portfolio save. Rename historical stats through an
idempotent startup migration before `/stock_dividend` gains its new meaning.
## Phases
| Phase | Name | Status |
|-------|------|--------|
| 1 | [Implement Ratio Dividend Commands](./phase-01-implement-ratio-dividend-commands.md) | Completed |
| 2 | [Migrate Command Statistics](./phase-02-migrate-command-statistics.md) | Completed |
| 3 | [Verify User Contracts](./phase-03-verify-user-contracts.md) | Completed |
## Dependencies
- Approved design: [Stock Dividend Command Brainstorm](../reports/260720-1612-stock-dividend-command-brainstorm.md)
- Evidence: [Dividend Notice Ratio Research](../reports/260720-1608-dividend-notice-ratio-research.md)
- Existing Go module, typed storage, `system` marker store, and stats startup path
## Contracts
- `/stock_cash_dividend <vnd_per_share> <TICKER>` credits a positive whole-VND amount per share.
- `/stock_share_dividend <owned:new> <TICKER>` accepts any positive integer ratio, preserves its entered form in replies, and adds floor-rounded shares; zero result rejects.
- `/stock_dividend <vnd_per_share> <owned:new> <TICKER>` applies both from pre-event holdings; zero shares still credits cash.
- Stats move `stock_dividend -> stock_cash_dividend` and `stock_bonus -> stock_share_dividend`; combined usage starts fresh.
- Commands are deliberate manual adjustments: repeated calls are allowed and
no issuer-event deduplication or ratio reduction is imposed.
## Validation Gate
- Focused stock, stats migration, startup, and command-menu tests.
- `gofmt` changed Go files; run `go test ./...` and `go vet ./...`.
- Run `golangci-lint run` when installed.
## Completion Criteria
- Commands, replies, menu JSON, and README agree on names and argument order.
- Ratio math cannot overflow and always floors; combined state persists once.
- Stats migration merges anonymous/per-user targets and repeated startup is a no-op.
- No unresolved questions.
## Validation Log
### Session 1 — 2026-07-20
**Trigger:** User requested `/ck:plan validate` before implementation.
**Questions asked:** 4
### Verification Results
- **Tier:** Standard (Fact Checker + Contract Verifier)
- **Claims checked:** 30
- **Verified:** 30 | **Failed:** 0 | **Unverified:** 0
- Verified current handlers/registry/portfolio storage, stats startup caller and
persisted row shape, shared system marker store, command-menu flow, tests,
README/JSON surfaces, and repository verification commands.
- Planned create-paths correctly do not exist yet: `dividends.go`,
`dividends_test.go`, and stats `startup_test.go`.
- **Failures:** None.
#### Questions & Answers
1. **[Assumption]** Should cash dividends require a positive whole VND amount per share and reject decimals?
- Options: Require positive whole VND (Recommended) | Preserve positive finite decimal input
- **Answer:** Require positive whole VND.
- **Rationale:** Matches approved manual-entry contract and avoids fractional VND credits.
2. **[Architecture]** Should share ratios accept any positive integer form and preserve the entered form without requiring reduction?
- Options: Accept and preserve any positive ratio (Recommended) | Require reduced ratios | Normalize replies
- **Answer:** Accept and preserve any positive ratio.
- **Rationale:** Mirrors issuer notices while avoiding unnecessary restrictions.
3. **[Scope]** Should repeated dividend commands be allowed without an issuer-event deduplication ledger?
- Options: Allow repeated manual adjustments (Recommended) | Add event IDs and deduplication
- **Answer:** Allow repeated manual adjustments; caller owns correctness.
- **Custom input:** "this is for user do manually, so not restrict anything, user call it will have reponsibility to make sure call it correct"
- **Rationale:** The feature is a manual portfolio adjustment, not an issuer-event ledger.
4. **[Risk]** Should migration retries scan prepared row checkpoints and retain all migration markers permanently?
- Options: Retry prepared rows and retain markers (Recommended) | Use global marker only | Clean row markers after completion
- **Answer:** Retry prepared rows and retain markers permanently.
- **Rationale:** Makes partial retries auditable and prevents double-counting.
#### Confirmed Decisions
- Manual responsibility does not bypass storage-safety validation: cash remains
positive whole VND; ratio parts remain positive integers; ticker and overflow
checks remain mandatory.
- No ratio canonicalization, notice lookup, event ID, or duplicate-call guard.
- Migration completion requires reconciling both source rows and prepared row
checkpoints; global and row markers remain as history.
#### Action Items
- [x] Propagate manual-input and repeat-call rules to Phase 1.
- [x] Propagate checkpoint retry/retention rules to Phase 2.
- [x] Propagate user-facing responsibility wording to Phase 3.
#### Impact on Phases
- Phase 1: tighten cash parsing; preserve ratio text; explicitly allow repeats.
- Phase 2: scan prepared checkpoints on retry; retain all markers.
- Phase 3: document manual responsibility and safety-validation boundary.
### Whole-Plan Consistency Sweep
- Files reread: `plan.md` and all three `phase-*.md` files.
- Decision deltas checked: 4.
- Reconciled stale references: 1 (`float acceptance` replaced by integer-input wording).
- Verified command names, input order, whole-VND rule, unreduced-ratio policy,
repeat-call policy, zero-share behavior, migration retry flow, and marker
retention agree across overview, requirements, steps, risks, and criteria.
- Unresolved contradictions: 0.
@@ -1,43 +0,0 @@
---
type: pm-completion-report
plan: stock-dividend-commands
status: completed
created_at: 2026-07-20T17:15:00+07:00
---
# Plan Complete: Stock Dividend Commands
## Summary
| Metric | Result |
|---|---|
| Plan status | Completed |
| Phase progress | 3/3 (100%) |
| Success criteria | 21/21 checked |
| Unresolved mappings | 0 |
| Review | 9/10, approved |
| Adversarial review | Passed |
## Delivered
- Explicit cash-only, share-only, and combined dividend contracts.
- Overflow-safe floor entitlement math using pre-event holdings and one save.
- Idempotent stats renames with retained migration checkpoints.
- Registry, Telegram menu, tests, and README aligned with public contracts.
## Verification
- Focused stock, stats, server, and command-menu tests: passed.
- `go test ./...`: passed.
- `go vet ./...`: passed.
- Whole-plan consistency sweep: passed; no unresolved mappings.
## Known Limitation
- MongoDB partial-boundary execution unavailable in the verification
environment. Deterministic retry behavior remains covered by available
focused verification; no completion blocker recorded.
## Unresolved Questions
None.
@@ -1,84 +0,0 @@
---
phase: 1
title: Design persistence and migration
status: completed
priority: P1
effort: medium
dependencies: []
---
# Phase 1: Design persistence and migration
## Overview
Extend both portfolio documents and add retry-safe startup migrations that
initialize legacy holdings from current quotes before command handlers run.
## Requirements
- Functional: persist `costBasis` as ticker/coin → total remaining cost.
- Functional: initialize missing legacy basis as `quantity × current price`.
- Functional: preserve any positive basis already written by a previous retry.
- Functional: scan every boot even after completion; the marker is an audit
record and never suppresses invariant checks.
- Non-functional: startup fails on a required quote/storage error; migration is
idempotent and bounded by a migration-wide deadline.
## Architecture
`cmd/server` checks which modules are loaded, then invokes each module's
`InitStore` before `modules.Install`. Every boot lists `user:` documents and
checks `positive holding => finite positive basis`. Migration fetches each
required symbol once, verifies the complete requested set, and writes only
missing basis using bounded CAS reload/retry. The shared `system` marker records
completion but does not skip future scans. Existing populated basis acts as
per-position retry progress.
## Related Code Files
- Modify: `internal/modules/stock/portfolio.go`
- Modify: `internal/modules/coin/portfolio.go`
- Create: `internal/modules/stock/startup.go`
- Create: `internal/modules/coin/startup.go`
- Modify: `cmd/server/main.go`
- Create: stock/coin startup memory and MongoDB tests
## Implementation Steps
1. Add and defensively initialize `CostBasis map[string]float64` with
`json:"costBasis" bson:"costBasis"` in both portfolio types.
2. Validate before normalization. Reject corrupt basis, noncanonical symbols,
canonical-key collisions, and non-finite holdings; do not delete or merge
ambiguous legacy assets.
3. Implement injectable stock and coin migrations with stable `system` keys,
`user:` listing, unique-symbol quote caching, complete quote-set validation,
bounded CAS reload/retry, progress logs, and a completion marker written last.
4. Wire migration only for loaded modules after registry construction and
before handler installation. Return startup-fatal errors with module/symbol
context but no credentials.
5. Apply an overall startup-migration timeout; timeout is startup-fatal and
leaves the marker incomplete.
6. Test empty, legacy, mixed, partial-retry, post-marker missing rows, partial
quote maps, noncanonical/corrupt rows, CAS conflicts, timeout, storage
failure, and idempotent second-boot cases in memory and MongoDB 8.
## Success Criteria
- [ ] New portfolios always contain initialized basis maps.
- [ ] Existing documents without `costBasis` still decode.
- [ ] Each legacy symbol receives migration-time current-price basis.
- [ ] Retry never overwrites basis initialized by an earlier attempt.
- [ ] Any required failure prevents the completion marker and aborts startup.
- [ ] Disabled modules do not call external quote providers.
- [ ] A completion marker never hides a later holding with missing basis.
## Risk Assessment
- External quote outages can block startup by explicit owner choice; provider
timeouts bound the delay and logs identify the module/symbol.
- Partial writes are recoverable because populated per-symbol basis is never
repriced and the global marker is written only after all rows succeed.
- Runtime validation fails closed if an old writer or restored row reintroduces
a positive holding without valid basis after startup.
- Storage listing remains unpaginated; the migration-wide deadline bounds the
current small, one-replica deployment without broad storage-API scope.
@@ -1,81 +0,0 @@
---
phase: 2
title: Implement weighted-average accounting
status: completed
priority: P1
effort: medium
dependencies:
- 1
---
# Phase 2: Implement weighted-average accounting
## Overview
Update stock and coin trade mutations to maintain remaining cost basis and show
realized P&L on every successful sale.
## Requirements
- Functional: buys add actual transaction spend to the symbol's total basis.
- Functional: sells remove proportional basis and report realized P&L.
- Functional: full exits remove both holding and basis keys.
- Functional: stock share dividends change quantity but not total basis.
- Non-functional: rejected trades and failed saves leave portfolio state intact.
- Non-functional: positive holdings with absent, nonpositive, or non-finite
basis fail closed instead of producing fabricated accounting.
## Architecture
For a sale, capture pre-sale holding and basis, then calculate:
```text
sold_basis = total_basis × sold_quantity / held_quantity
realized_pnl = proceeds - sold_basis
realized_pct = realized_pnl / sold_basis × 100
```
The remaining average price is unchanged after a partial sale. A share dividend
adds shares without cost, so `basis / new_quantity` automatically lowers the
average price. No lot ledger or cumulative realized-P&L field is introduced.
## Related Code Files
- Modify: stock/coin `portfolio.go`, trade handlers, and format helpers
- Modify: stock dividend handler tests to assert unchanged total basis
- Modify: stock/coin portfolio and handler tests
## Implementation Steps
1. Add small portfolio methods to add purchase basis and remove proportional
basis with finite/overflow guards appropriate to each module's precision.
2. Update buys so quantity, cash deduction, and basis addition persist in the
same mutation.
3. Update sells so quantity, cash credit, and basis reduction persist together;
append `Realized P&L` amount and percentage to the reply.
4. Determine coin full exit from normalized post-sale holdings. If quantity
disappears at the dust threshold, assign all remaining basis to that sale
and delete the basis key. Keep monetary validation separate from quantity
dust normalization.
5. Assert cash/share/combined dividend behavior: cash never changes basis and
share additions preserve total basis.
6. Cover weighted repeated buys, gain/loss/breakeven partial sells, full sells,
insufficient holdings, overflow/invalid data, conflicts, and save failure.
## Success Criteria
- [ ] `average price == remaining basis / remaining quantity` after every buy,
sell, and share dividend.
- [ ] Stock and coin sell replies report correct realized P&L.
- [ ] Full exits leave no holding or basis entry.
- [ ] Failed/rejected operations do not mutate persisted state.
- [ ] Missing or corrupt runtime basis blocks affected accounting operations
with a clear retry/operator message.
## Risk Assessment
- Stock uses float64 currency already; calculations must reject non-finite or
unsafe results. Coin retains its existing dust normalization.
- Mutation order must not delete quantity before sold basis is calculated.
- Existing account `Meta.Invested` continues to mean deposits and must not be
repurposed as position basis.
@@ -1,86 +0,0 @@
---
phase: 3
title: Verify P&L behavior and compatibility
status: completed
priority: P1
effort: medium
dependencies:
- 1
- 2
---
# Phase 3: Verify P&L behavior and compatibility
## Overview
Expose average entry price and unrealized position P&L in both portfolio views,
document the accounting model, and run full compatibility verification.
## Requirements
- Functional: each priced holding shows average price and unrealized P&L.
- Functional: retain cash, total value, invested deposits, and account P&L.
- Functional: unavailable quotes degrade gracefully without inventing P&L;
aggregate account P&L is suppressed when any holding is unpriced.
- Non-functional: output remains deterministic and within Telegram limits.
## Architecture
Position metrics derive from persisted remaining basis:
```text
average_price = basis / quantity
unrealized_pnl = current_value - basis
unrealized_pct = unrealized_pnl / basis × 100
```
Label position performance `Unrealized P&L` and the existing net-worth minus
top-ups metric `Account P&L` so dividends, cash, and realized proceeds are not
confused with open-position performance. If any holding lacks a quote, label
priced values as partial and do not render aggregate account P&L.
## Related Code Files
- Modify: `internal/modules/stock/handlers.go`, `stats_test.go`
- Modify: `internal/modules/coin/views.go` and view/handler tests
- Modify: `README.md`, `docs/deploy-coolify-selfhosted.md`
- Create: completion journal during finalization
## Implementation Steps
1. Render average basis and signed unrealized P&L for each priced stock/coin
position. Add explicit stock sorting; retain coin sorting.
2. Keep existing total-value and invested calculations; relabel the final line
as `Account P&L` and add aggregate `Unrealized P&L` for priced positions.
3. For unavailable current quotes, show stored average price when valid, mark
current value/P&L unavailable, label priced totals partial, and suppress
aggregate account P&L.
4. Add a conservative Telegram reply budget with deterministic truncation and
test worst-case multi-position output.
5. Update exact output, missing-price, reply-budget, migration compatibility,
and command behavior tests.
6. Document startup migration, weighted-average basis, realized vs unrealized
P&L, and dividend effects.
7. Run `gofmt`, focused stock/coin/server tests, MongoDB Testcontainers tests,
`go test -count=1 ./...`, `go vet ./...`, `go build ./...`, and
`golangci-lint run`; complete independent test/debug/review gates.
## Success Criteria
- [ ] Each open position shows average price and unrealized amount/percentage.
- [ ] Portfolio retains account-level P&L with a clearer label.
- [ ] Missing prices do not fabricate P&L or break the reply.
- [ ] Stock output is sorted and both portfolio replies remain within Telegram
limits under worst-case supported holdings.
- [ ] Legacy memory and MongoDB documents remain readable and migrate once.
- [ ] All focused and repository-wide verification passes.
- [ ] No command name, parameter, stats-history, or price-provider contract
changes.
## Risk Assessment
- Users may confuse account and position metrics; explicit labels and README
examples define both.
- Missing quotes make totals partial today; the new output must label them
partial and suppress account P&L rather than report a false loss.
- Longer replies require budget tests for multi-asset coin portfolios.
@@ -1,108 +0,0 @@
---
title: Stock and Coin Cost Basis P&L
description: >-
Persist weighted-average cost basis, migrate legacy holdings at startup
prices, and expose realized/unrealized P&L.
status: completed
priority: P1
branch: main
tags:
- stock
- coin
- mongodb
- migration
- pnl
blockedBy: []
blocks: []
created: '2026-07-21T08:08:21.155Z'
createdBy: 'ck:plan'
source: skill
---
# Stock and Coin Cost Basis P&L
## Overview
Add a per-symbol remaining cost basis to stock and coin portfolios. New buys
increase basis, partial sells remove proportional weighted-average basis, and
full sells remove it. Portfolio views show average entry price and unrealized
P&L per position; sell replies show realized P&L for that sale while preserving
the existing account-level P&L.
Legacy holdings migrate before handlers are installed. Each missing basis is
initialized from the current quote, so its unrealized P&L starts at zero.
Migration is idempotent, marker-backed, and fail-fast: the bot does not accept
trades if required quotes or storage writes fail.
## Phases
| Phase | Name | Status |
|-------|------|--------|
| 1 | [Design persistence and migration](./phase-01-design-persistence-and-migration.md) | Completed |
| 2 | [Implement weighted-average accounting](./phase-02-implement-weighted-average-accounting.md) | Completed |
| 3 | [Verify P&L behavior and compatibility](./phase-03-verify-p-l-behavior-and-compatibility.md) | Completed |
## Dependencies
- No active plan dependencies. The completed stock-dividend implementation
defines the share-dividend behavior this plan must preserve.
- MongoDB 8 Testcontainers are required for persisted migration verification;
existing Docker-unavailable warning/skip behavior remains unchanged.
## Decisions
- Persist total remaining cost per symbol, not a mutable average-price field.
- Derive average price as `basis / held quantity`.
- Use proportional weighted-average basis for partial sells.
- Keep total stock basis unchanged when share dividends add quantity.
- Keep cash dividends and top-ups outside position basis.
- Do not persist cumulative realized P&L or trade lots.
- Run a module migration only when that module is loaded; disabled-module data
migrates the next time the module is enabled.
- Abort startup on migration failure to avoid operating with unknown basis.
## Acceptance Criteria
- Stock and coin portfolios persist remaining cost basis without breaking
legacy BSON/JSON decoding.
- Repeated buys produce the correct weighted average; partial/full sells update
basis exactly and report realized gain or loss.
- Startup maintenance scans every boot, initializes only missing legacy basis
from one complete current quote per symbol, never reprices completed work,
and writes its system marker only after success.
- Share dividends add quantity without adding cost; cash dividends do not
change position basis.
- Portfolio output shows average price and per-position unrealized P&L plus the
existing account-level P&L.
- No command names or parameter contracts change.
- Focused tests, MongoDB migration tests, full tests, vet, build, and lint pass.
## Red Team Review
### Session — 2026-07-21
**Findings:** 12 deduplicated (9 accepted, 3 rejected)
| Finding | Severity | Disposition | Applied To |
|---|---|---|---|
| Completion marker cannot enforce future row invariants | Critical | Accept | Phases 12 |
| Missing quotes fabricate aggregate account P&L | Critical | Accept | Phase 3 |
| Migration has no overall deadline | High | Accept | Phase 1 |
| Partial stock quote maps can look successful | High | Accept | Phase 1 |
| Corrupt/noncanonical legacy symbols are ambiguous | High | Accept | Phase 1 |
| Migration conflicts lack retry semantics | Medium | Accept | Phase 1 |
| Coin quantity dust can orphan monetary basis | Medium | Accept | Phase 2 |
| Invalid basis can be silently normalized | Medium | Accept | Phases 12 |
| Stock output order and reply size are unsafe | Medium | Accept | Phase 3 |
| Storage-wide pagination and migration lease | High | Reject | Deadline and fail-closed runtime validation fit the one-replica deployment |
| Persist quote manifests and correction tooling | High | Reject | Outside paper-trading scope; validate complete finite quotes and log context |
| Restrict portfolio commands to private chats | Medium | Reject | Pre-existing visibility contract outside this accounting change |
### Whole-Plan Consistency Sweep
- Files reread: `plan.md` and all three phase files.
- Decision deltas checked: every-boot invariant scan, fail-closed runtime checks,
bounded startup, quote completeness, corrupt-symbol rejection, CAS retry,
dust cleanup, missing-price suppression, sorting, and reply bounds.
- Reconciled stale references: marker short-circuiting and partial aggregate P&L.
- Unresolved contradictions: 0.
@@ -1,165 +0,0 @@
---
type: brainstorm-report
topic: stock-dividend-command-naming
created_at: 2026-07-20T16:12:33+07:00
status: approved
modes:
html: false
wiki: false
---
# Brainstorm Report: Stock Dividend Commands
## Summary
Approved three explicit commands:
```text
/stock_cash_dividend <vnd_per_share> <TICKER>
/stock_share_dividend <owned:new> <TICKER>
/stock_dividend <vnd_per_share> <owned:new> <TICKER>
```
Cash uses VND per existing share. Share distributions use the official-notice
ratio form, such as `4:1` or `100:10`. Mixed dividends calculate both outcomes
from the pre-event holding and persist them atomically.
## Problem-First Analysis
### Solution-Jumping Diagnosis
The original names encoded outcomes inconsistently: `/stock_bonus` added shares
while `/stock_dividend` added cash. Users could not distinguish a true bonus
share event from a stock dividend or represent a mixed dividend notice.
### Underlying Problem
Users need command names and inputs that map directly to Vietnamese dividend
notices, while preserving simple manual paper-portfolio accounting.
### Assumptions and Validation
| Assumption | Risk if wrong | Validation |
|---|---|---|
| Notices provide share ratios | Users must calculate quantities | Confirmed by VSDC `4:1` and `100:10` notices |
| Cash per share is sufficient | Percentage input may be expected | Notices explicitly state VND received per share |
| Fractions round down | Portfolio could over-credit shares | Confirmed by both VSDC examples |
| Mixed event uses one record-date holding | Cash may include newly issued shares | Calculate both before mutation |
### Problem Statement
Paper-trading users cannot accurately record cash-only, share-only, and mixed
dividends because current command semantics are incomplete and `stock_bonus`
does not necessarily mean a stock dividend. Success means each notice maps to
one obvious command and produces auditable integer-share and cash results.
### Alternative Framings
1. Keep current commands: smallest change, but mixed events need two commands
and terminology remains ambiguous.
2. Use explicit cash/share commands plus a combined command: clear, direct,
matches actual notices. Selected.
3. Add a generic corporate-action command with subtypes: extensible but too
complex for three small manual operations.
### Evidence Status
Strong for syntax and rounding: two recent official VSDC mixed-dividend notices
use cash-per-share explanations, `owned:new` ratios, and floor rounding.
### Validation Plan
- Unit-test ratio parsing, overflow boundaries, and floor division.
- Table-test cash-only, share-only, and combined handlers.
- Verify combined calculation uses the same pre-event holding.
- Test one-time stats migrations for anonymous and per-user rows.
- Reject the design if real notices require unsupported fractional settlement
rather than discarded fractions.
### Stakeholder Message
Use explicit cash and share commands for single-form notices, and the combined
command for mixed notices. Inputs mirror VSDC wording, reducing manual math and
making bot replies easy to compare against source notices.
## Evaluated Approaches
| Approach | Pros | Cons | Decision |
|---|---|---|---|
| Absolute new-share quantity | Reuses current handler behavior | Manual calculation; hides rounding | Reject |
| Percentage share input | Familiar shorthand | Ambiguous; less faithful to notices | Reject |
| `owned:new` share ratio | Mirrors notices; deterministic | Needs parser and integer safety | Approve |
## Approved Behavior
### Cash-only
```text
/stock_cash_dividend 1500 IDC
cash = existing_shares * 1500 VND
```
### Share-only
```text
/stock_share_dividend 100:10 IDC
new_shares = floor(existing_shares * 10 / 100)
```
Reject a share-only event when the valid ratio produces zero new shares and
report the minimum holding needed.
### Mixed
```text
/stock_dividend 1500 100:10 IDC
```
Calculate cash and new shares from the same pre-event holding. When the share
result is zero, still credit cash and report zero new shares. Save once so cash
and shares cannot diverge after a partial failure.
## Compatibility and Touchpoints
- Rename `/stock_bonus` to `/stock_share_dividend`.
- Move historical `/stock_bonus` stats to `/stock_share_dividend`.
- Move historical cash-only `/stock_dividend` stats to
`/stock_cash_dividend` before reusing `/stock_dividend` for mixed events.
- Update `internal/modules/stock/stock.go`, handlers, handler tests, and
`telegram-commands.json`.
- Add idempotent startup migration through the shared `system` collection;
cover anonymous rows, per-user rows, target-row merging, and repeated startup.
## Risks
- Integer multiplication can overflow before division; validate bounds or use a
safe quotient/remainder calculation.
- Reusing `/stock_dividend` without migrating old stats mislabels historical
cash-only usage as combined usage.
- Applying shares before cash calculation overpays the same event.
- Accepting decimal or zero ratio components creates undefined rounding.
## Success Criteria
- `4:1` and `100:10` parse; malformed or non-positive ratios fail clearly.
- Share calculations use integer floor semantics.
- Cash/share/mixed commands match their documented examples.
- Mixed updates are atomic and use pre-event holdings.
- Existing command statistics remain preserved under the correct new meanings.
- Focused tests, `go test ./...`, and `go vet ./...` pass.
## Sources
- [VSDC IDC mixed dividend notice](https://www.vsd.vn/vi/ad/197421)
- [VSDC PTB mixed dividend notice](https://vsd.vn/vi/ad/195203)
- [Supporting research report](./260720-1608-dividend-notice-ratio-research.md)
## Next Steps
Create a tests-first implementation plan because this change renames public
commands, changes persisted stats attribution, and modifies financial
calculation behavior.
## Unresolved Questions
None.
@@ -1,48 +0,0 @@
# Portfolio Cleanup Completion Report
## Summary
| Item | Result |
|---|---|
| Coin dividend cursor | Removed from schema, API, validation, callers, tests |
| Stock dividend cursor | Retained unchanged |
| Portfolio migrations | Completed runtime paths and migration-only tests removed |
| Stats rename migration | Removed; recurring indexes retained |
| LoL startup maintenance | TTL index retained |
| Historical system data | Untouched; reusable helper retained |
| Dividend API research | SSI iBoard recommended behind provider interface |
## Verification
- Focused and full Go tests passed.
- MongoDB 8 stats-index and LoL TTL-index tests executed and passed.
- `go vet ./...`, `go build ./...`, and `golangci-lint run` passed.
- `git diff --check` passed.
- Independent tester, debugger, and reviewer reported no defects.
- Coin BSON regression coverage proves a stale cursor loads safely and is
omitted by the next whole-document encoding/write.
## Documentation
- README and deployment guide now distinguish stock and coin asset schemas.
- Standalone dividend API research report added under `plans/reports/`.
- Existing completed implementation plans remain historical records; no phase
statuses changed.
## Known Limitations
- Untouched MongoDB coin documents retain the ignored cursor until their next
portfolio write.
- SSI iBoard corporate actions are undocumented and have no published SLA.
## Next Steps
1. Commit the approved cleanup when requested.
2. Design the Telegram dividend-event selection/confirmation interaction.
3. Implement SSI behind a replaceable provider interface after command design
approval.
## Unresolved Questions
- Should dividend lookup inspect one ticker per command or all stock assets?
- Should an event only prefill guidance or execute after explicit confirmation?
@@ -1,57 +0,0 @@
---
type: project-status-report
topic: stock-dividend-event-buttons
reported_at: 2026-07-21T18:15:00+07:00
status: completed
---
# Stock Dividend Event Buttons Completion
## Summary
`/stock_portfolio` now checks SSI dividend events for held tickers after sending
the portfolio. Relevant events receive owner-bound, expiring Apply buttons;
successful checks with no events remain silent.
## Completed
- [x] Replaceable SSI provider with pagination, local classification, exact
share ratios, event-ID deduplication, and bounded date overlap.
- [x] Per-ticker cursor checks that preserve concurrent portfolio writes.
- [x] Opaque 24-hour pending actions bound to caller, chat, and message.
- [x] Current-holding cash/share application with replay-safe event ledger.
- [x] Position lifecycle binding that rejects buttons after full sale/reopen.
- [x] Shared callback registration, visibility validation, and authorization.
- [x] README and regression tests updated.
## Verification
| Gate | Result |
|---|---|
| Focused stock/modules tests | Passed |
| Full `go test -count=1 ./...` | Passed |
| Stock race and concurrency stress tests | Passed |
| `go vet ./...` | Passed |
| `golangci-lint run` | Passed, 0 issues |
| `go build ./...` | Passed |
| Independent final review | Passed, no blockers |
## Plan Sync
No active phase plan maps to this incremental request. Existing stock-dividend
and cost-basis plans were already completed before this work and remain
unchanged. Session task tracking is fully complete.
## Remaining Risks
- SSI iBoard is undocumented and best-effort.
- SSI publication timestamps are day-granularity; a bounded overlap prevents
same-day loss, while pending actions and applied IDs suppress duplicates.
- The portfolio has no dated-lot ledger, so acceptance uses holdings at click
time rather than legal record-date entitlement.
- Manual dividend commands cannot attach SSI event IDs; users must not apply
the same event both manually and through its button.
## Next Step
Commit only after explicit user approval.