docs(stock): document dividend commands

This commit is contained in:
2026-07-20 17:33:10 +07:00
parent ed0ef83601
commit 2761d39e62
12 changed files with 949 additions and 0 deletions
+14
View File
@@ -19,6 +19,20 @@ Atlas via long polling and an in-process cron scheduler.
Disable modules with the `MODULES` environment variable.
### Stock dividend commands
Stock dividends are manual portfolio adjustments:
- `/stock_cash_dividend <vnd_per_share> <TICKER>` credits a positive whole-VND amount for each pre-event share held. Example: `/stock_cash_dividend 1500 TCB`.
- `/stock_share_dividend <owned:new> <TICKER>` adds `floor(pre_event_shares × new / owned)` whole shares. Example: `/stock_share_dividend 100:10 TCB`.
- `/stock_dividend <vnd_per_share> <owned:new> <TICKER>` applies both parts from the same pre-event holding and saves them together. Example: `/stock_dividend 1500 100:10 TCB`.
Ratios use `owned:new` exactly as written in the issuer notice. Equivalent
unreduced ratios are accepted and the entered ratio is preserved in the reply.
The bot validates syntax, tickers, and arithmetic safety, but does not look up
notices or prevent duplicate calls. The caller is responsible for verifying the
notice and avoiding accidental repeated adjustments.
## Layout
```
@@ -0,0 +1,34 @@
# 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.
@@ -0,0 +1,33 @@
# 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.
@@ -0,0 +1,31 @@
# 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.
@@ -0,0 +1,67 @@
# 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.
@@ -0,0 +1,109 @@
---
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.
@@ -0,0 +1,113 @@
---
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.
@@ -0,0 +1,95 @@
---
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.
@@ -0,0 +1,133 @@
---
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.
@@ -0,0 +1,43 @@
---
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.
@@ -0,0 +1,112 @@
---
type: research-report
topic: dividend-notice-ratio-examples
conducted_at: 2026-07-20T16:08:21+07:00
status: complete
---
# Research Report: Dividend Notice Ratio Examples
## Summary
Official VSDC notices support ratio-based share-dividend input. Notices express
the ratio as existing shares or rights to new shares, such as `4:1` and
`100:10`. Resulting fractional shares are rounded down and discarded.
Keep cash input as VND per existing share. Although notices also state a cash
percentage, that percentage is based on par value; accepting the actual VND per
share avoids needing par-value data.
## Methodology
- Sources: 2 recent official VSDC notices
- Notice dates: 2026-05-05 and 2026-06-26
- Terms: `trả cổ tức bằng tiền`, `trả cổ tức bằng cổ phiếu`, `tỷ lệ thực hiện`
- Scope: input semantics and rounding for the three proposed Telegram commands
## Findings
### IDC mixed dividend
- Cash: 15% per share, explicitly `1 share receives 1,500 VND`.
- Shares: `100:10`, meaning 100 existing rights receive 10 new shares.
- Example: 139 existing shares produce `139 / 100 * 10 = 13.9`, rounded down
to 13 new shares; the 0.9 fraction is discarded.
Source: [IDC: cash and stock dividends for 2025](https://www.vsd.vn/vi/ad/197421)
### PTB mixed dividend
- Cash: 5% per share, explicitly `1 share receives 500 VND`.
- Shares: `4:1`, meaning 4 existing shares receive 1 new share.
- Example: 2,026 existing shares produce `2,026 / 4 * 1 = 506.5`, rounded down
to 506 new shares; the 0.5 fraction is discarded.
Source: [PTB: cash and stock dividends for 2025](https://vsd.vn/vi/ad/195203)
## Comparative Analysis
| Input | Benefit | Problem |
|---|---|---|
| Absolute new-share quantity | Matches current `/stock_bonus` behavior | User must calculate entitlement manually |
| Percentage | Familiar for rates such as 10% | Ambiguous parsing and less faithful to notice wording |
| `owned:new` ratio | Mirrors VSDC notices; easy to audit | Requires integer parsing and floor rounding |
Recommendation: accept the `owned:new` ratio.
## Command Recommendation
```text
/stock_cash_dividend <vnd_per_share> <TICKER>
/stock_share_dividend <owned:new> <TICKER>
/stock_dividend <vnd_per_share> <owned:new> <TICKER>
```
Examples:
```text
/stock_cash_dividend 1500 IDC
/stock_share_dividend 100:10 IDC
/stock_dividend 1500 100:10 IDC
/stock_cash_dividend 500 PTB
/stock_share_dividend 4:1 PTB
/stock_dividend 500 4:1 PTB
```
Calculation:
```text
new_shares = floor(existing_shares * new / owned)
cash_vnd = existing_shares * vnd_per_share
```
For a mixed dividend, calculate both values from the same pre-event holding,
then save cash and shares atomically.
## Validation and Pitfalls
- Require positive whole-number ratio parts; reject `0:1`, `4:0`, negatives,
decimals, missing colon, and extra colons.
- Reduce equivalent ratios internally if useful, but preserve the entered ratio
in the reply for auditability.
- Use integer arithmetic and floor division for shares; avoid floating-point
rounding.
- Reject events producing zero new shares, or explicitly confirm zero payout;
recommended behavior: reject with the minimum holding required.
- Calculate cash before adding new shares so newly distributed shares do not
receive cash from the same event.
- Keep cash input in VND/share. Do not derive it from a percentage unless the
portfolio also stores the security's par value.
## Next Steps
1. Confirm zero-share-result behavior.
2. Finalize the three command contracts and reply wording.
3. Plan command renames, combined atomic update, stats migrations, and tests.
## Unresolved Questions
- When a valid ratio yields zero new shares, should the command reject the
event or record a zero-share result while still applying cash in the combined
command?
@@ -0,0 +1,165 @@
---
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.