feat: security tab with derived holding and full execution history - #4257
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a dedicated Security tab on a security business page, backed by a new GraphQL query that returns a derived position summary plus the full execution history, and reuses a shared executions table UI between the charge panel and the new tab.
Changes:
- Server: add
Query.securityBusinessHistory(businessId)returning security metadata, derivedSecurityPosition, and execution rows paired to transactions/charges. - Server: add
calculateSecurityPosition()(with tests) to derive quantity/average cost/totals and the history span. - Client: introduce a shared
SecurityExecutionsTablecomponent and add the new Business “Security” tab UI using the new query.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/server/src/modules/foreign-securities/types.ts | Adds proto types for security business history + derived position. |
| packages/server/src/modules/foreign-securities/typeDefs/security-businesses.graphql.ts | Extends schema with securityBusinessHistory and related types. |
| packages/server/src/modules/foreign-securities/resolvers/security-businesses.resolver.ts | Implements query + field resolvers for history executions and position formatting. |
| packages/server/src/modules/foreign-securities/providers/foreign-securities.provider.ts | Adds DB query + matching logic to fetch full security history and pair executions to transactions. |
| packages/server/src/modules/foreign-securities/providers/tests/foreign-securities.integration.test.ts | Updates provider test construction for the new dependency. |
| packages/server/src/modules/foreign-securities/helpers/security-position.helper.ts | Introduces the position derivation logic from executions. |
| packages/server/src/modules/foreign-securities/helpers/tests/security-position.helper.test.ts | Adds unit tests for position derivation cases. |
| packages/client/src/components/securities/security-executions-table.tsx | New shared executions table + GraphQL fragment for consistent rendering. |
| packages/client/src/components/charges/extended-info/foreign-securities-info.tsx | Reuses the shared executions table component in the charge panel. |
| packages/client/src/components/business/security-section.tsx | New Security tab content: position summary + full execution history with charge links. |
| packages/client/src/components/business/index.tsx | Adds a “Security” tab trigger/content when securityInfo is present. |
| codegen.ts | Maps new GraphQL types to server proto types for resolvers/codegen. |
| .changeset/security-business-tab.md | Changeset documenting the new security business page/tab feature. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| totalBought: position => formatFinancialAmount(position.totalBought, position.currency), | ||
| totalSold: position => formatFinancialAmount(position.totalSold, position.currency), |
There was a problem hiding this comment.
Good catch — fixed in the latest push. All three amounts now go through a positionAmount(value, currency) helper that returns null when either is missing, so averageCost keeps its existing guard and the two totals gain the same one.
The client needed no change: the card already renders position.totalBought?.formatted ?? "—", so it was faithfully displaying a wrong value rather than missing a null case. Pinned the contract the resolver leans on in the position helper test (no executions ⇒ currency: null).
9742d03 to
4442f0b
Compare
5a78f8c to
5e176c4
Compare
fed3740 to
cb23ed0
Compare
5e176c4 to
0b787ee
Compare
0b787ee to
566a682
Compare
Adds Query.securityBusinessHistory and a Security tab on the business page, shown for any business with a businesses_securities row. Shows the position derived from ingested executions - with the as-of caveat, since holdings are not scraped - and every execution across the security's Poalim keys, each linking to the charge behind its cash movement. The charge panel's portfolio table and the new one are now one shared component over one fragment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses review feedback. A security with no ingested executions has no currency, and formatFinancialAmount falls back to the local one - so the card showed a confident ILS 0 for totals nothing is known about. The resolver now returns null for all three amounts when the currency is null; the client already renders null as an em dash. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
566a682 to
e137d21
Compare
🚀 Snapshot Release (
|
| Package | Version | Info |
|---|---|---|
@accounter/client |
0.1.0-alpha-20260820145547-9aa709e6e4b6848f940ba48f372d24aecfccc955 |
npm ↗︎ unpkg ↗︎ |
@accounter/green-invoice-graphql |
0.8.7-alpha-20260820145547-9aa709e6e4b6848f940ba48f372d24aecfccc955 |
npm ↗︎ unpkg ↗︎ |
@accounter/hashavshevet-mesh |
0.2.13-alpha-20260820145547-9aa709e6e4b6848f940ba48f372d24aecfccc955 |
npm ↗︎ unpkg ↗︎ |
@accounter/israeli-vat-scraper |
0.1.13-alpha-20260820145547-9aa709e6e4b6848f940ba48f372d24aecfccc955 |
npm ↗︎ unpkg ↗︎ |
@accounter/modern-poalim-scraper |
0.11.0-alpha-20260820145547-9aa709e6e4b6848f940ba48f372d24aecfccc955 |
npm ↗︎ unpkg ↗︎ |
@accounter/payper-mesh |
0.2.13-alpha-20260820145547-9aa709e6e4b6848f940ba48f372d24aecfccc955 |
npm ↗︎ unpkg ↗︎ |
@accounter/scraper-app |
0.0.3-alpha-20260820145547-9aa709e6e4b6848f940ba48f372d24aecfccc955 |
npm ↗︎ unpkg ↗︎ |
@accounter/server |
0.2.0-alpha-20260820145547-9aa709e6e4b6848f940ba48f372d24aecfccc955 |
npm ↗︎ unpkg ↗︎ |
@accounter/shaam-uniform-format-generator |
0.2.7-alpha-20260820145547-9aa709e6e4b6848f940ba48f372d24aecfccc955 |
npm ↗︎ unpkg ↗︎ |
@accounter/shaam6111-generator |
0.1.9-alpha-20260820145547-9aa709e6e4b6848f940ba48f372d24aecfccc955 |
npm ↗︎ unpkg ↗︎ |
Step 6 of the per-security businesses plan. Stacked on #4256.
A security business had no view of its own — trades were visible one charge at a time, and the only
way to see what is held was to add the executions up by hand. The business page now grows a
Security tab, shown for any business with a
businesses_securitiesrow:with the ISIN, symbol, exchange, currency, type and ETF/foreign badges.
linking to the charge behind its cash movement (
—when nothing matched).The position is derived, and says so
Holdings are not scraped, so the card states the date the ingested history starts from and that
anything held before it is not counted. Corporate actions that change the unit count without an
execution row are invisible for the same reason. Cash-only actions (dividends, interest) leave the
count alone; buys, distributions and transfers in add; sales, redemptions and transfers out subtract.
Server
Query.securityBusinessHistory(businessId)→SecurityBusinessHistory(SecurityPosition+SecurityHistoryExecution, an execution with its transaction and charge). The pairing is the one thecharge view already shows, read from the other end: the security business's own transactions matched
against its executions by
matchExecutionsToTransactions(#4253). The tab runs its own query — likeCharges/Transactions/Ledger — so a business page never pays for execution history it doesn't show.
Shared table
The charge panel's "Portfolio activity" table and the new one are now one component
(
components/securities/security-executions-table.tsx) over one fragment, so the two always readalike. The charge panel changes in no other way.
Verification
yarn generate,yarn lint,yarn prettierclean; server and client both typecheck.yarn test— 3439 passed, incl. 8 new position cases (buys minus sells, weighted average cost,cash-only actions leaving the count alone, transfers in/out, redemption as both an exit and
proceeds, history span independent of row order).
yarn test:integration— 3882 passed.🤖 Generated with Claude Code