feat: per-lot cost-basis strategies for tax reports (closes #511) - #645
Merged
thlpkee20-wq merged 2 commits intoJul 29, 2026
Merged
Conversation
…#511) Implement per-lot cost-basis tracking with pluggable FIFO/LIFO/HIFO disposal strategies for tax reporting. - Add investment_lots and disposals database migrations (020, 021) - Implement CostBasisStrategy interface with FIFO/LIFO/HIFO strategies - Add InvestmentLotRepository and DisposalRepository with FOR UPDATE locking - Create TaxationService with atomic disposal processing - Add TaxationHandler with 5 API endpoints (dispose, preview, gains-summary, list lots, create lot) - Mount routes at /api/v1/taxation with JWT auth middleware - 85 tests across 4 test suites, all passing - Comprehensive documentation in docs/cost-basis-strategies.md
|
@Praiz089017 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #511
Per-Lot Cost-Basis Tracking with FIFO/LIFO/HIFO Strategies
Summary
Implements comprehensive per-lot cost-basis tracking for tax reporting with pluggable disposal strategies (FIFO, LIFO, HIFO). Adds investment lot lifecycle tables, immutable historical evaluations, per-jurisdiction gains summaries, and a strategy selector with forward-only change enforcement.
Closes: #511
Features Implemented
Core Domain
cost_basis_per_unit,acquired_at, andquantity. Lots trackremaining_quantityand transition throughopen → partially_used → exhaustedlifecycle states.CostBasisStrategyinterface. New strategies can be added by implementing the interface and registering inSTRATEGY_REGISTRY.remaining_quantityandstatusare updated as disposals consume lots.Database
investment_lotstable: Lot lifecycle tracking withNUMERIC(36,18)quantities (fractional shares to 18 decimal places) andNUMERIC(30,10)monetary precision.disposalstable: Immutable disposal records capturing cost basis at time of disposal, realized gain/loss, strategy used, and tax-report linkage.Security & Correctness
SELECT ... FOR UPDATErow-level locking within database transactions prevents concurrent disposals from double-spending lots.authMiddleware()— every request requires a valid Bearer token.AppErrorwith properVALIDATION_ERRORcodes — no upstream errors leak to clients.API Design
POST /taxation/previewlets investors see exactly which lots would be consumed before committing.API Endpoints
All endpoints are mounted at
{API_VERSION_PREFIX}/taxation(e.g.,/api/v1/taxation) and require JWT authentication.POST/dispose201withDisposalResult(allocations, weighted average cost basis, realized gain/loss)POST/preview200withDisposalResultpreviewGET/gains-summary200withJurisdictionGainsSummary[]GET/lots200withInvestmentLot[]POST/lots201withInvestmentLotExample: Process a Disposal
Request:
Response (201):
{ "message": "Disposal processed successfully", "data": { "allocations": [ { "lot": { "id": "lot-1", "acquired_at": "2024-01-15T00:00:00Z", "cost_basis_per_unit": 10, "remaining_quantity": 50 }, "quantityConsumed": 50, "costBasisPerUnit": 10, "totalCostBasis": 500 } ], "totalQuantityDisposed": 50, "weightedAverageCostBasis": 10.0, "totalCostBasis": 500.0, "realizedGainLoss": 250.0, "strategy": "FIFO" } }Example: Gains Summary
Response (200):
{ "message": "Gains summary retrieved successfully", "data": [ { "jurisdiction": "US", "totalProceeds": 5000, "totalCostBasis": 3000, "totalRealizedGainLoss": 2000, "disposalCount": 5, "strategyBreakdown": { "FIFO": { "count": 3, "totalGainLoss": 1000 }, "LIFO": { "count": 2, "totalGainLoss": 1000 }, "HIFO": { "count": 0, "totalGainLoss": 0 } } } ] }Architecture
Disposal Strategy Comparison
Database Schema
investment_lotsidUUID PKinvestor_idUUID NOT NULLoffering_idUUID NOT NULLinvestment_idUUID NOT NULL REFERENCES investments(id)assetVARCHAR(255) NOT NULLquantityNUMERIC(36,18) NOT NULL CHECK (> 0)cost_basis_per_unitNUMERIC(30,10) NOT NULL CHECK (>= 0)total_cost_basisNUMERIC(30,10) NOT NULL CHECK (>= 0)quantity × cost_basis_per_unitremaining_quantityNUMERIC(36,18) NOT NULL CHECK (>= 0)cost_currencyVARCHAR(10) DEFAULT 'USD'acquired_atTIMESTAMPTZ NOT NULLjurisdictionVARCHAR(10) DEFAULT 'US'statusVARCHAR(20) CHECK (open/partially_used/exhausted)disposalsidUUID PKlot_idUUID NOT NULL REFERENCES investment_lots(id)quantity_disposedNUMERIC(36,18) NOT NULL CHECK (> 0)cost_basis_per_unitNUMERIC(30,10)total_cost_basisNUMERIC(30,10)proceedsNUMERIC(30,10)realized_gain_lossNUMERIC(30,10)proceeds - total_cost_basisdisposal_price_per_unitNUMERIC(30,10)strategyVARCHAR(10) CHECK (FIFO/LIFO/HIFO)disposed_atTIMESTAMPTZtax_report_finalizedBOOLEAN DEFAULT FALSETesting
Coverage: 85 tests across 4 test suites — all passing
costBasisStrategies.test.tstaxationService.test.tsinvestmentLotRepository.test.tsFOR UPDATElocking, status transitions, quantity trackingdisposalRepository.test.tsEdge Cases Covered
cost_basis_per_unit = 0remaining_quantity = 0are ignoredrealizedGainLosscalculationsFiles Changed
New Files (14)
src/db/migrations/020_create_investment_lots.sqlinvestment_lotstable with indices and triggersrc/db/migrations/021_create_disposals.sqldisposalstable with indices and triggersrc/services/taxation/types.tsCostBasisStrategy,InvestmentLot,Disposal,DisposalResult,JurisdictionGainsSummary,LotAllocation,ProcessDisposalInputsrc/services/taxation/costBasisStrategies.tsFIFOStrategy,LIFOStrategy,HIFOStrategyimplementations +STRATEGY_REGISTRY+resolveStrategy()src/services/taxation/costBasisStrategies.test.tssrc/services/taxation/taxationService.tsTaxationService: atomic disposal processing, lot creation, jurisdiction summaries, previewsrc/services/taxation/taxationService.test.tssrc/db/repositories/investmentLotRepository.tsInvestmentLotRepository: CRUD, transactional operations,FOR UPDATElockingsrc/db/repositories/investmentLotRepository.test.tssrc/db/repositories/disposalRepository.tsDisposalRepository: CRUD, jurisdiction-level aggregation with shared helpersrc/db/repositories/disposalRepository.test.tssrc/handlers/taxationHandler.tsTaxationHandler: 5 HTTP handlers with input validation and error mappingsrc/routes/taxation.tsauthMiddleware()router-level guarddocs/cost-basis-strategies.mdModified Files (1)
src/index.tsimport taxationRouter from './routes/taxation'andapp.use(API_VERSION_PREFIX + '/taxation', taxationRouter)mountSecurity Assumptions
Trust Boundaries
authMiddleware()before handler invocationAppErrorinstances — database failures and strategy errors map toVALIDATION_ERRORorINTERNAL_ERRORSELECT ... FOR UPDATEwithin transactions prevents concurrent double-spending of lotscost_basis_per_unit,quantity,acquired_atare write-once; disposal records are append-onlyinvestor_id,offering_id, andlot_id— no email, name, or other PIIThreat Mitigations
FOR UPDATElock until the first commitsAppErrorcodes — no raw database errors or stack traces reach the client$1, $2, ...) — no string interpolationFuture Hardening Identified
TypeScript Type Safety
npx tsc --noEmitpasses for all taxation code)let strategy: CostBasisStrategyandlet allocations: LotAllocation[](notany)// @ts-ignoreoras anycasts in production codeQueryResult<InvestmentLotRow>andQueryResult<DisposalRow>genericsPerformance Considerations
FOR UPDATElocks are scoped to a single(investor_id, offering_id)pair, limiting the blast radius of concurrent disposals.getJurisdictionGainsSummaryandgetJurisdictionGainsSummaryByOfferingdelegate aggregation to SQLGROUP BYfor efficiency — no N+1 queries.Migration Guide
# Apply both migrations npm run migrateThe
020_create_investment_lots.sqland021_create_disposals.sqlmigrations are additive — they create new tables with no impact on existing data. No data migration is required.Deployment Checklist
020_create_investment_lots.sql,021_create_disposals.sql)npm test -- --testPathPatterns='costBasis|taxation|disposal|investmentLot'— 85 tests passingnpx tsc --noEmit— zero type errorsdocs/cost-basis-strategies.mdLabels
backend— Backend implementationtax-reporting— Tax/cost-basis featuredatabase— New migration and repository layersecurity— Row-level locking, error sanitization, JWT authtesting— 85 tests with comprehensive edge case coveragedocumentation— Feature docs and inline JSDocRisk Assessment
Low Risk: This is a new feature with no modifications to existing code paths (only additive changes). The single modified file (
src/index.ts) adds one router mount — no existing routes are affected. All new code is behind JWT authentication and has comprehensive test coverage.Related Issues
PUT /taxation/strategywith approval workflow)Ready for review and merge! 🚀