@@ -19,8 +19,10 @@ import { getQueueMetrics } from '../queue/queueMetrics.js'
1919import { getErrorMessage , getErrorObject , parseOptionalBoolean } from '../utils/helpers.js'
2020import { createPortfolioSchema } from './validation.js'
2121import { rebalanceLockService } from '../services/rebalanceLock.js'
22+ import { REBALANCE_STRATEGIES } from '../services/rebalancingStrategyService.js'
2223import type { Portfolio } from '../types/index.js'
2324import { ok , fail } from '../utils/apiResponse.js'
25+ import { getPortfolioExport } from '../services/portfolioExportService.js'
2426
2527const router = Router ( )
2628const stellarService = new StellarService ( )
@@ -47,6 +49,10 @@ const parseHistorySource = (value: unknown): 'offchain' | 'simulated' | 'onchain
4749}
4850
4951
52+ router . get ( '/strategies' , ( _req : Request , res : Response ) => {
53+ return ok ( res , { strategies : REBALANCE_STRATEGIES } )
54+ } )
55+
5056router . get ( '/rebalance/history' , async ( req : Request , res : Response ) => {
5157 try {
5258 const portfolioId = req . query . portfolioId as string
@@ -122,6 +128,7 @@ router.post('/rebalance/history/sync-onchain', requireAdmin, async (req: Request
122128 }
123129} )
124130
131+ router . post ( '/portfolio' , writeRateLimiter , idempotencyMiddleware , async ( req : Request , res : Response ) => {
125132
126133 try {
127134 const parsed = createPortfolioSchema . safeParse ( req . body )
@@ -139,9 +146,17 @@ router.post('/rebalance/history/sync-onchain', requireAdmin, async (req: Request
139146 : message
140147 return fail ( res , 400 , 'VALIDATION_ERROR' , fullMessage )
141148 }
149+ const { userAddress, allocations, threshold, slippageTolerance, strategy, strategyConfig } = parsed . data
142150
143151 const slippageTolerancePercent = slippageTolerance ?? 1
144- const portfolioId = await stellarService . createPortfolio ( userAddress , allocations , threshold , slippageTolerancePercent )
152+ const portfolioId = await stellarService . createPortfolio (
153+ userAddress ,
154+ allocations ,
155+ threshold ,
156+ slippageTolerancePercent ,
157+ strategy ?? 'threshold' ,
158+ strategyConfig ?? { }
159+ )
145160 const mode = featureFlags . demoMode ? 'demo' : 'onchain'
146161 return ok ( res , {
147162 portfolioId,
@@ -154,21 +169,50 @@ router.post('/rebalance/history/sync-onchain', requireAdmin, async (req: Request
154169 }
155170} )
156171
172+ router . get ( '/portfolio/:id' , async ( req : Request , res : Response ) => {
157173
158174 try {
159175 const portfolioId = req . params . id
160176 if ( ! portfolioId ) return fail ( res , 400 , 'VALIDATION_ERROR' , 'Portfolio ID required' )
161177 const portfolio = await stellarService . getPortfolio ( portfolioId )
162178 if ( ! portfolio ) return fail ( res , 404 , 'NOT_FOUND' , 'Portfolio not found' )
163- main
179+
164180 return ok ( res , { portfolio } )
165181 } catch ( error ) {
166182 logger . error ( '[ERROR] Get portfolio failed' , { error : getErrorObject ( error ) } )
167183 return fail ( res , 500 , 'INTERNAL_ERROR' , getErrorMessage ( error ) )
168184 }
169185} )
170186
171- main
187+ // Portfolio export (JSON, CSV, PDF) — GDPR data portability
188+ router . get ( '/portfolio/:id/export' , requireJwtWhenEnabled , async ( req : Request , res : Response ) => {
189+ try {
190+ const portfolioId = req . params . id
191+ const format = ( req . query . format as string ) ?. toLowerCase ( )
192+ if ( ! portfolioId ) return fail ( res , 400 , 'VALIDATION_ERROR' , 'Portfolio ID required' )
193+ if ( ! [ 'json' , 'csv' , 'pdf' ] . includes ( format ) ) {
194+ return fail ( res , 400 , 'VALIDATION_ERROR' , 'Query parameter format must be one of: json, csv, pdf' )
195+ }
196+ const result = await getPortfolioExport ( portfolioId , format as 'json' | 'csv' | 'pdf' )
197+ if ( ! result ) return fail ( res , 404 , 'NOT_FOUND' , 'Portfolio not found' )
198+ res . setHeader ( 'Content-Type' , result . contentType )
199+ res . setHeader ( 'Content-Disposition' , `attachment; filename="${ result . filename } "` )
200+ if ( Buffer . isBuffer ( result . body ) ) {
201+ return res . send ( result . body )
202+ }
203+ return res . send ( result . body )
204+ } catch ( error ) {
205+ logger . error ( '[ERROR] Portfolio export failed' , { error : getErrorObject ( error ) } )
206+ return fail ( res , 500 , 'INTERNAL_ERROR' , getErrorMessage ( error ) )
207+ }
208+ } )
209+
210+ router . get ( '/user/:address/portfolios' , async ( req : Request , res : Response ) => {
211+ try {
212+ const address = req . params . address
213+ if ( ! address ) return fail ( res , 400 , 'VALIDATION_ERROR' , 'User address required' )
214+ const list = portfolioStorage . getUserPortfolios ( address )
215+
172216 return ok ( res , { portfolios : list } )
173217 } catch ( error ) {
174218 logger . error ( '[ERROR] Get user portfolios failed' , { error : getErrorObject ( error ) } )
@@ -200,6 +244,7 @@ router.get('/portfolio/:id/rebalance-plan', async (req: Request, res: Response)
200244} )
201245
202246// Manual portfolio rebalance
247+ router . post ( '/portfolio/:id/rebalance' , writeRateLimiter , idempotencyMiddleware , async ( req : Request , res : Response ) => {
203248
204249 try {
205250 const portfolioId = req . params . id ;
@@ -692,6 +737,7 @@ router.get('/portfolio/:id/performance-summary', async (req: Request, res: Respo
692737router . post ( '/notifications/subscribe' , requireJwtWhenEnabled , writeRateLimiter , idempotencyMiddleware , async ( req : Request , res : Response ) => {
693738 try {
694739 const userId = req . user ?. address ?? req . body ?. userId
740+ const { emailEnabled, webhookEnabled, webhookUrl, events, emailAddress } = req . body ?? { }
695741
696742 // Validation
697743 if ( ! userId ) {
0 commit comments