diff --git a/.eslintrc.js b/.eslintrc.js index c54b3f74..da41d675 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -14,7 +14,7 @@ module.exports = { node: true, jest: true, }, - ignorePatterns: ['.eslintrc.js'], + ignorePatterns: ['.eslintrc.js', 'test/'], rules: { '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', @@ -23,5 +23,13 @@ module.exports = { "@typescript-eslint/no-namespace": "off", "@typescript-eslint/no-unused-vars": "warn", "@typescript-eslint/no-var-requires": "off", + "no-restricted-imports": ["error", { + "patterns": [ + { + "group": ["../../*", "../../../*"], + "message": "Use absolute imports with 'src/' prefix instead of relative parent imports that cross domain boundaries. Example: use 'src/user/entities/user.entity' instead of '../../user/entities/user.entity'. Same-module parent imports (../file) and same-directory imports (./file) are still allowed." + } + ] + }] }, -}; +}; \ No newline at end of file diff --git a/src/audit/entities/agent-event.entity.ts b/src/audit/entities/agent-event.entity.ts index 0b038f20..2eb56d8e 100644 --- a/src/audit/entities/agent-event.entity.ts +++ b/src/audit/entities/agent-event.entity.ts @@ -7,7 +7,7 @@ import { ManyToOne, JoinColumn, } from "typeorm"; -import { User } from "../../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; export enum AgentEventType { CREATED = "created", diff --git a/src/audit/entities/compute-result.entity.ts b/src/audit/entities/compute-result.entity.ts index e81848a0..800ace79 100644 --- a/src/audit/entities/compute-result.entity.ts +++ b/src/audit/entities/compute-result.entity.ts @@ -8,7 +8,7 @@ import { ManyToOne, JoinColumn, } from "typeorm"; -import { User } from "../../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; export enum ComputeResultStatus { PENDING = "pending", diff --git a/src/audit/entities/oracle-submission.entity.ts b/src/audit/entities/oracle-submission.entity.ts index 198df42a..ee0d05c0 100644 --- a/src/audit/entities/oracle-submission.entity.ts +++ b/src/audit/entities/oracle-submission.entity.ts @@ -8,7 +8,7 @@ import { ManyToOne, JoinColumn, } from "typeorm"; -import { User } from "../../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; export enum OracleSubmissionStatus { PENDING = "pending", diff --git a/src/audit/entities/provenance-record.entity.ts b/src/audit/entities/provenance-record.entity.ts index 73c7c579..9ac3c49c 100644 --- a/src/audit/entities/provenance-record.entity.ts +++ b/src/audit/entities/provenance-record.entity.ts @@ -7,7 +7,7 @@ import { ManyToOne, JoinColumn, } from "typeorm"; -import { User } from "../../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; export enum ProvenanceStatus { PENDING = "pending", diff --git a/src/audit/guards/provenance-access.guard.ts b/src/audit/guards/provenance-access.guard.ts index 925550ba..5030543e 100644 --- a/src/audit/guards/provenance-access.guard.ts +++ b/src/audit/guards/provenance-access.guard.ts @@ -4,7 +4,7 @@ import { ExecutionContext, ForbiddenException, } from "@nestjs/common"; -import { UserRole } from "../../user/entities/user.entity"; +import { UserRole } from "src/user/entities/user.entity"; /** * Guard that ensures users can only access their own provenance records. diff --git a/src/auth/auth-core.module.ts b/src/auth/auth-core.module.ts index a4e61c9c..c8b99fd4 100644 --- a/src/auth/auth-core.module.ts +++ b/src/auth/auth-core.module.ts @@ -20,7 +20,7 @@ import { JwtStrategy } from "./jwt.strategy"; import { JwtAuthGuard } from "./jwt.guard"; import { TokenBlacklistService } from "./token-blacklist.service"; -import { User } from "../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; @Module({ imports: [ @@ -40,11 +40,6 @@ import { User } from "../user/entities/user.entity"; // @deprecated Use StrategyAuthService (via AuthStrategiesModule) for new code. AuthService, ], - exports: [ - TokenBlacklistService, - JwtAuthGuard, - AuthService, - JwtModule, - ], + exports: [TokenBlacklistService, JwtAuthGuard, AuthService, JwtModule], }) export class AuthCoreModule {} diff --git a/src/auth/auth-strategies.module.ts b/src/auth/auth-strategies.module.ts index 521fd627..5c6d43cc 100644 --- a/src/auth/auth-strategies.module.ts +++ b/src/auth/auth-strategies.module.ts @@ -29,14 +29,10 @@ import { TraditionalStrategy } from "./strategies/traditional/traditional.strate import { OAuthStrategy } from "./strategies/oauth/oauth.strategy"; import { ApiKeyStrategy } from "./strategies/api-key/api-key.strategy"; -import { User } from "../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; @Module({ - imports: [ - ConfigModule, - AuthCoreModule, - TypeOrmModule.forFeature([User]), - ], + imports: [ConfigModule, AuthCoreModule, TypeOrmModule.forFeature([User])], providers: [ StrategyRegistry, StrategyAuthService, diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 09194d9c..95b52adc 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -31,7 +31,7 @@ import { SessionRecoveryService } from "./session-recovery.service"; import { DelegationService } from "./delegation.service"; import { EnhancedAuthService } from "./enhanced-auth.service"; -import { User } from "../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; import { EmailVerification } from "./entities/email-verification.entity"; import { Wallet } from "./entities/wallet.entity"; import { RefreshToken, TwoFactorAuth } from "./entities/auth.entity"; diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts index 0e52b091..62d5e8aa 100644 --- a/src/auth/auth.service.spec.ts +++ b/src/auth/auth.service.spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from "@nestjs/testing"; import { AuthService } from "./auth.service"; import { JwtService } from "@nestjs/jwt"; import { getRepositoryToken } from "@nestjs/typeorm"; -import { User, UserRole } from "../user/entities/user.entity"; +import { User, UserRole } from "src/user/entities/user.entity"; import { TokenBlacklistService } from "./token-blacklist.service"; import { Repository } from "typeorm"; import { diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index d726471b..b2b520d2 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -9,7 +9,7 @@ import { Repository } from "typeorm"; import * as bcrypt from "bcrypt"; import { v4 as uuidv4 } from "uuid"; import { JwtService } from "@nestjs/jwt"; -import { User } from "../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; import { RegisterDto, LoginDto } from "./dto/auth.dto"; import { TokenBlacklistService } from "./token-blacklist.service"; diff --git a/src/auth/delegation.service.ts b/src/auth/delegation.service.ts index e982108c..6ee9222a 100644 --- a/src/auth/delegation.service.ts +++ b/src/auth/delegation.service.ts @@ -11,7 +11,7 @@ import { InjectRepository } from "@nestjs/typeorm"; import { Repository, LessThan } from "typeorm"; import { randomBytes, createHash } from "crypto"; import { Wallet, WalletStatus, WalletType } from "./entities/wallet.entity"; -import { User } from "../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; export interface DelegationRequest { delegatorWalletId: string; diff --git a/src/auth/email-linking.service.ts b/src/auth/email-linking.service.ts index ffaa0a59..5a39ebc7 100644 --- a/src/auth/email-linking.service.ts +++ b/src/auth/email-linking.service.ts @@ -8,7 +8,7 @@ import { import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { randomBytes } from "crypto"; -import { User } from "../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; import { EmailVerification } from "./entities/email-verification.entity"; import { EmailService } from "./email.service"; diff --git a/src/auth/entities/auth.entity.ts b/src/auth/entities/auth.entity.ts index 5809d593..31415fe4 100644 --- a/src/auth/entities/auth.entity.ts +++ b/src/auth/entities/auth.entity.ts @@ -8,7 +8,7 @@ import { JoinColumn, Index, } from "typeorm"; -import { User } from "../../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; @Entity("refresh_tokens") export class RefreshToken { diff --git a/src/auth/entities/wallet.entity.ts b/src/auth/entities/wallet.entity.ts index 404c34d9..57cf2865 100644 --- a/src/auth/entities/wallet.entity.ts +++ b/src/auth/entities/wallet.entity.ts @@ -8,7 +8,7 @@ import { ManyToOne, JoinColumn, } from "typeorm"; -import { User } from "../../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; export enum WalletStatus { ACTIVE = "active", diff --git a/src/auth/guards/strategy-auth.guard.ts b/src/auth/guards/strategy-auth.guard.ts index 1cf42e5f..b08c8a9c 100644 --- a/src/auth/guards/strategy-auth.guard.ts +++ b/src/auth/guards/strategy-auth.guard.ts @@ -9,7 +9,7 @@ import { ConfigService } from "@nestjs/config"; import { Reflector } from "@nestjs/core"; import { StrategyRegistry } from "../strategies/strategy.registry"; import { AuthPayload } from "../strategies/interfaces/auth-strategy.interface"; -import { IS_PUBLIC_KEY } from "../../common/decorators/public.decorator"; +import { IS_PUBLIC_KEY } from "src/common/decorators/public.decorator"; import { ALLOWED_STRATEGIES_KEY } from "../decorators/allowed-strategies.decorator"; /** diff --git a/src/auth/session-recovery.service.ts b/src/auth/session-recovery.service.ts index 4d926674..e8603440 100644 --- a/src/auth/session-recovery.service.ts +++ b/src/auth/session-recovery.service.ts @@ -10,7 +10,7 @@ import { Repository } from "typeorm"; import { randomBytes, createHash, scryptSync } from "crypto"; import { JwtService } from "@nestjs/jwt"; import { Wallet, WalletStatus } from "./entities/wallet.entity"; -import { User } from "../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; import { ChallengeService } from "./challenge.service"; import { EmailService } from "./email.service"; diff --git a/src/auth/strategies/api-key/api-key.strategy.ts b/src/auth/strategies/api-key/api-key.strategy.ts index ca45418a..700d6433 100644 --- a/src/auth/strategies/api-key/api-key.strategy.ts +++ b/src/auth/strategies/api-key/api-key.strategy.ts @@ -15,7 +15,7 @@ import { AuthPayload, ApiKeyCredentials, } from "../interfaces/auth-strategy.interface"; -import { User } from "../../../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; /** * API Key metadata diff --git a/src/auth/strategies/oauth/oauth.strategy.ts b/src/auth/strategies/oauth/oauth.strategy.ts index aea30a0c..08c7e5a2 100644 --- a/src/auth/strategies/oauth/oauth.strategy.ts +++ b/src/auth/strategies/oauth/oauth.strategy.ts @@ -14,7 +14,7 @@ import { AuthPayload, OAuthCredentials, } from "../interfaces/auth-strategy.interface"; -import { User } from "../../../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; /** * OAuth provider configuration diff --git a/src/auth/strategies/traditional/traditional.strategy.ts b/src/auth/strategies/traditional/traditional.strategy.ts index b1b9d4b7..5ac4ec2c 100644 --- a/src/auth/strategies/traditional/traditional.strategy.ts +++ b/src/auth/strategies/traditional/traditional.strategy.ts @@ -16,7 +16,7 @@ import { AuthPayload, TraditionalCredentials, } from "../interfaces/auth-strategy.interface"; -import { User } from "../../../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; /** * Traditional email/password authentication strategy diff --git a/src/auth/strategies/wallet/wallet.strategy.ts b/src/auth/strategies/wallet/wallet.strategy.ts index c0344018..63ab4425 100644 --- a/src/auth/strategies/wallet/wallet.strategy.ts +++ b/src/auth/strategies/wallet/wallet.strategy.ts @@ -15,8 +15,8 @@ import { AuthPayload, WalletCredentials, } from "../interfaces/auth-strategy.interface"; -import { ChallengeService } from "../../challenge.service"; -import { User } from "../../../user/entities/user.entity"; +import { ChallengeService } from "src/auth/challenge.service"; +import { User } from "src/user/entities/user.entity"; /** * Wallet-based authentication strategy diff --git a/src/auth/wallet-auth.service.spec.ts b/src/auth/wallet-auth.service.spec.ts index aef71dfd..cb83a7b2 100644 --- a/src/auth/wallet-auth.service.spec.ts +++ b/src/auth/wallet-auth.service.spec.ts @@ -3,7 +3,7 @@ import { WalletAuthService } from "./wallet-auth.service"; import { ChallengeService } from "./challenge.service"; import { JwtService } from "@nestjs/jwt"; import { getRepositoryToken } from "@nestjs/typeorm"; -import { User, UserRole } from "../user/entities/user.entity"; +import { User, UserRole } from "src/user/entities/user.entity"; import { Wallet } from "./entities/wallet.entity"; import { Repository } from "typeorm"; import { diff --git a/src/auth/wallet-auth.service.ts b/src/auth/wallet-auth.service.ts index 554502b7..e1f9a156 100644 --- a/src/auth/wallet-auth.service.ts +++ b/src/auth/wallet-auth.service.ts @@ -11,7 +11,7 @@ import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { verifyMessage } from "ethers"; import { ChallengeService } from "./challenge.service"; -import { User } from "../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; import { Wallet, WalletStatus, WalletType } from "./entities/wallet.entity"; export interface AuthPayload { diff --git a/src/common/guard/nonce.guard.spec.ts b/src/common/guard/nonce.guard.spec.ts index 68801a05..3168e56c 100644 --- a/src/common/guard/nonce.guard.spec.ts +++ b/src/common/guard/nonce.guard.spec.ts @@ -6,7 +6,7 @@ import { } from "@nestjs/common"; import { getRepositoryToken } from "@nestjs/typeorm"; import { NonceGuard } from "./nonce.guard"; -import { SubmissionNonce } from "../../oracle/entities/submission-nonce.entity"; +import { SubmissionNonce } from "src/oracle/entities/submission-nonce.entity"; const mockNonceRepository = { findOne: jest.fn(), diff --git a/src/common/guard/nonce.guard.ts b/src/common/guard/nonce.guard.ts index fe12b5f3..abe3213e 100644 --- a/src/common/guard/nonce.guard.ts +++ b/src/common/guard/nonce.guard.ts @@ -7,7 +7,7 @@ import { } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; -import { SubmissionNonce } from "../../oracle/entities/submission-nonce.entity"; +import { SubmissionNonce } from "src/oracle/entities/submission-nonce.entity"; /** * Guard to prevent replay attacks by validating nonces diff --git a/src/common/guard/portfolio-owner.guard.ts b/src/common/guard/portfolio-owner.guard.ts index d2042f9e..70939490 100644 --- a/src/common/guard/portfolio-owner.guard.ts +++ b/src/common/guard/portfolio-owner.guard.ts @@ -7,7 +7,7 @@ import { } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; -import { Portfolio } from "../../portfolio/entities/portfolio.entity"; +import { Portfolio } from "src/portfolio/entities/portfolio.entity"; @Injectable() export class PortfolioOwnerGuard implements CanActivate { diff --git a/src/common/guard/quota.guard.ts b/src/common/guard/quota.guard.ts index c1af4e04..4ce2f6db 100644 --- a/src/common/guard/quota.guard.ts +++ b/src/common/guard/quota.guard.ts @@ -11,7 +11,7 @@ import { RATE_LIMIT_KEY, RateLimitOptions, } from "../decorators/rate-limit.decorator"; -import { QUOTA_LEVELS, DEFAULT_QUOTA } from "../../config/quota.config"; +import { QUOTA_LEVELS, DEFAULT_QUOTA } from "src/config/quota.config"; @Injectable() export class RateLimiterService { diff --git a/src/config/logger.ts b/src/config/logger.ts index e3185546..99d327ac 100644 --- a/src/config/logger.ts +++ b/src/config/logger.ts @@ -41,6 +41,8 @@ export const logger = pino({ // Helper function to create child loggers with context export const createLogger = (context: Record) => { const traceId = getCurrentTraceId(); - const contextWithTrace = traceId ? { ...context, trace_id: traceId } : context; + const contextWithTrace = traceId + ? { ...context, trace_id: traceId } + : context; return logger.child(contextWithTrace); -}; \ No newline at end of file +}; diff --git a/src/config/tracing.ts b/src/config/tracing.ts index abf3825c..356b9399 100644 --- a/src/config/tracing.ts +++ b/src/config/tracing.ts @@ -3,12 +3,18 @@ import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentation import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; import { JaegerExporter } from "@opentelemetry/exporter-jaeger"; import { resourceFromAttributes } from "@opentelemetry/resources"; -import { - BatchSpanProcessor, +import { + BatchSpanProcessor, TraceIdRatioBasedSampler, - SpanProcessor + SpanProcessor, } from "@opentelemetry/sdk-trace-base"; -import { trace, SpanStatusCode, Span, context, propagation } from "@opentelemetry/api"; +import { + trace, + SpanStatusCode, + Span, + context, + propagation, +} from "@opentelemetry/api"; import { W3CTraceContextPropagator } from "@opentelemetry/core"; // Rate-limited sampling configuration @@ -16,9 +22,11 @@ import { W3CTraceContextPropagator } from "@opentelemetry/core"; // For production, consider implementing custom adaptive sampling based on your needs const createConfiguredSampler = () => { const samplingRate = parseFloat(process.env.OTEL_SAMPLING_RATE || "1.0"); - const minSamplingRate = parseFloat(process.env.OTEL_MIN_SAMPLING_RATE || "0.1"); + const minSamplingRate = parseFloat( + process.env.OTEL_MIN_SAMPLING_RATE || "0.1", + ); const finalRate = Math.max(Math.min(samplingRate, 1.0), minSamplingRate); - + console.log(`Configured sampling rate: ${finalRate * 100}%`); return new TraceIdRatioBasedSampler(finalRate); }; @@ -28,7 +36,9 @@ const createSpanProcessor = (): SpanProcessor => { // Jaeger exporter configuration (default enabled) if (process.env.OTEL_EXPORTER_JAEGER_ENABLED !== "false") { const jaegerExporter = new JaegerExporter({ - endpoint: process.env.OTEL_EXPORTER_JAEGER_ENDPOINT || "http://localhost:14268/api/traces", + endpoint: + process.env.OTEL_EXPORTER_JAEGER_ENDPOINT || + "http://localhost:14268/api/traces", }); console.log("Jaeger exporter configured"); return new BatchSpanProcessor(jaegerExporter); @@ -37,7 +47,9 @@ const createSpanProcessor = (): SpanProcessor => { // OTLP exporter configuration (for other backends) if (process.env.OTEL_EXPORTER_OTLP_ENABLED === "true") { const otlpExporter = new OTLPTraceExporter({ - url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4318/v1/traces", + url: + process.env.OTEL_EXPORTER_OTLP_ENDPOINT || + "http://localhost:4318/v1/traces", }); console.log("OTLP exporter configured"); return new BatchSpanProcessor(otlpExporter); @@ -79,7 +91,11 @@ export const startTracing = async () => { try { sdk.start(); console.log("OpenTelemetry tracing initialized with configurable sampling"); - console.log("Jaeger endpoint:", process.env.OTEL_EXPORTER_JAEGER_ENDPOINT || "http://localhost:14268/api/traces"); + console.log( + "Jaeger endpoint:", + process.env.OTEL_EXPORTER_JAEGER_ENDPOINT || + "http://localhost:14268/api/traces", + ); console.log("Jaeger UI available at:", "http://localhost:16686"); } catch (err) { console.error("Failed to start OpenTelemetry SDK:", err); @@ -143,15 +159,15 @@ export const createSpan = async ( // Manual span creation example (for documentation) /** * Example usage of manual span creation: - * + * * await createSpan("process-user-data", async (span) => { * span.setAttribute("user.id", userId); * span.setAttribute("operation", "data-processing"); - * + * * // Create child span for nested operation * return await createSpan("validate-user-input", async (childSpan) => { * childSpan.setAttribute("input.size", inputData.length); * return validateInput(inputData); * }, { "operation.type": "validation" }); * }, { "module": "user-service" }); - */ \ No newline at end of file + */ diff --git a/src/config/typeorm.config.ts b/src/config/typeorm.config.ts index bce0653c..0168fa5d 100644 --- a/src/config/typeorm.config.ts +++ b/src/config/typeorm.config.ts @@ -1,22 +1,22 @@ import { DataSource } from "typeorm"; -import { User } from "../user/entities/user.entity"; -import { EmailVerification } from "../auth/entities/email-verification.entity"; -import { Wallet } from "../auth/entities/wallet.entity"; -import { SignedPayload } from "../oracle/entities/signed-payload.entity"; -import { SubmissionNonce } from "../oracle/entities/submission-nonce.entity"; -import { AgentEvent } from "../audit/entities/agent-event.entity"; -import { OracleSubmission } from "../audit/entities/oracle-submission.entity"; -import { ComputeResult } from "../audit/entities/compute-result.entity"; -import { ProvenanceRecord } from "../audit/entities/provenance-record.entity"; -import { DeFiPosition } from "../defi/entities/defi-position.entity"; -import { DeFiYieldRecord } from "../defi/entities/defi-yield-record.entity"; -import { DeFiTransaction } from "../defi/entities/defi-transaction.entity"; -import { DeFiYieldStrategy } from "../defi/entities/defi-yield-strategy.entity"; -import { DeFiRiskAssessment } from "../defi/entities/defi-risk-assessment.entity"; -import { Alert } from "../alerts/entities/alert.entity"; -import { AlertTriggerLog } from "../alerts/entities/alert-trigger-log.entity"; -import { AlertPreference } from "../alerts/entities/alert-preference.entity"; -import { AlertDeliveryLog } from "../alerts/entities/alert-delivery-log.entity"; +import { User } from "src/user/entities/user.entity"; +import { EmailVerification } from "src/auth/entities/email-verification.entity"; +import { Wallet } from "src/auth/entities/wallet.entity"; +import { SignedPayload } from "src/oracle/entities/signed-payload.entity"; +import { SubmissionNonce } from "src/oracle/entities/submission-nonce.entity"; +import { AgentEvent } from "src/audit/entities/agent-event.entity"; +import { OracleSubmission } from "src/audit/entities/oracle-submission.entity"; +import { ComputeResult } from "src/audit/entities/compute-result.entity"; +import { ProvenanceRecord } from "src/audit/entities/provenance-record.entity"; +import { DeFiPosition } from "src/defi/entities/defi-position.entity"; +import { DeFiYieldRecord } from "src/defi/entities/defi-yield-record.entity"; +import { DeFiTransaction } from "src/defi/entities/defi-transaction.entity"; +import { DeFiYieldStrategy } from "src/defi/entities/defi-yield-strategy.entity"; +import { DeFiRiskAssessment } from "src/defi/entities/defi-risk-assessment.entity"; +import { Alert } from "src/alerts/entities/alert.entity"; +import { AlertTriggerLog } from "src/alerts/entities/alert-trigger-log.entity"; +import { AlertPreference } from "src/alerts/entities/alert-preference.entity"; +import { AlertDeliveryLog } from "src/alerts/entities/alert-delivery-log.entity"; export default new DataSource({ type: "postgres", diff --git a/src/defi/entities/defi-position.entity.ts b/src/defi/entities/defi-position.entity.ts index 713cd0cd..3756dd6f 100644 --- a/src/defi/entities/defi-position.entity.ts +++ b/src/defi/entities/defi-position.entity.ts @@ -8,7 +8,7 @@ import { UpdateDateColumn, Index, } from "typeorm"; -import { User } from "../../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; import { DeFiYieldRecord } from "./defi-yield-record.entity"; import { DeFiTransaction } from "./defi-transaction.entity"; diff --git a/src/defi/entities/defi-yield-strategy.entity.ts b/src/defi/entities/defi-yield-strategy.entity.ts index bbc53fe5..d1d4c5cf 100644 --- a/src/defi/entities/defi-yield-strategy.entity.ts +++ b/src/defi/entities/defi-yield-strategy.entity.ts @@ -7,7 +7,7 @@ import { UpdateDateColumn, Index, } from "typeorm"; -import { User } from "../../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; export enum StrategyType { HIGHEST_YIELD = "highest_yield", diff --git a/src/instrument.ts b/src/instrument.ts index b06cc667..c5b9c021 100644 --- a/src/instrument.ts +++ b/src/instrument.ts @@ -3,13 +3,11 @@ import { nodeProfilingIntegration } from "@sentry/profiling-node"; Sentry.init({ dsn: process.env.SENTRY_DSN, - integrations: [ - nodeProfilingIntegration(), - ], + integrations: [nodeProfilingIntegration()], // Performance Monitoring - tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0, + tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0, // Set sampling rate for profiling - this is relative to tracesSampleRate - profilesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0, - environment: process.env.NODE_ENV || 'development', + profilesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0, + environment: process.env.NODE_ENV || "development", release: process.env.npm_package_version || "1.0.0", }); diff --git a/src/migrations/1704067200000-CreatePortfolioTransactionsTable.ts b/src/migrations/1704067200000-CreatePortfolioTransactionsTable.ts index ef7aec22..360274fc 100644 --- a/src/migrations/1704067200000-CreatePortfolioTransactionsTable.ts +++ b/src/migrations/1704067200000-CreatePortfolioTransactionsTable.ts @@ -28,7 +28,16 @@ export class CreatePortfolioTransactionsTable1704067200000 implements MigrationI { name: "type", type: "enum", - enum: ["buy", "sell", "transfer", "dividend", "stake", "unstake", "deposit", "withdrawal"], + enum: [ + "buy", + "sell", + "transfer", + "dividend", + "stake", + "unstake", + "deposit", + "withdrawal", + ], isNullable: false, }, { diff --git a/src/portfolio/dto/portfolio-asset.dto.ts b/src/portfolio/dto/portfolio-asset.dto.ts index 95c9f756..c6c227e0 100644 --- a/src/portfolio/dto/portfolio-asset.dto.ts +++ b/src/portfolio/dto/portfolio-asset.dto.ts @@ -1,10 +1,19 @@ -import { IsString, IsOptional, IsNumber, Matches, Length, IsEnum } from "class-validator"; +import { + IsString, + IsOptional, + IsNumber, + Matches, + Length, + IsEnum, +} from "class-validator"; import { Chain } from "../entities/portfolio-asset.entity"; export class PortfolioAssetDto { @IsString() @Length(3, 10) - @Matches(/^[A-Z0-9]+$/, { message: "ticker must be 3-10 alphanumeric characters" }) + @Matches(/^[A-Z0-9]+$/, { + message: "ticker must be 3-10 alphanumeric characters", + }) ticker: string; @IsString() @@ -30,7 +39,9 @@ export class PortfolioAssetDto { export class AddAssetToPortfolioDto { @IsString() @Length(3, 10) - @Matches(/^[A-Z0-9]+$/, { message: "ticker must be 3-10 alphanumeric characters" }) + @Matches(/^[A-Z0-9]+$/, { + message: "ticker must be 3-10 alphanumeric characters", + }) ticker: string; @IsString() diff --git a/src/portfolio/dto/transaction.dto.ts b/src/portfolio/dto/transaction.dto.ts index b4e3cef6..a073a1cc 100644 --- a/src/portfolio/dto/transaction.dto.ts +++ b/src/portfolio/dto/transaction.dto.ts @@ -9,7 +9,11 @@ import { Min, IsObject, } from "class-validator"; -import { Transaction, TransactionType, TransactionStatus } from "../entities/transaction.entity"; +import { + Transaction, + TransactionType, + TransactionStatus, +} from "../entities/transaction.entity"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; export class CreateTransactionDto { @@ -82,7 +86,9 @@ export class CreateTransactionDto { @IsDateString() transactionDate?: string; - @ApiPropertyOptional({ description: "Idempotency key for preventing duplicates" }) + @ApiPropertyOptional({ + description: "Idempotency key for preventing duplicates", + }) @IsOptional() @IsString() idempotencyKey?: string; @@ -92,7 +98,9 @@ export class CreateTransactionDto { @IsObject() metadata?: Record; - @ApiPropertyOptional({ description: "Total transaction value (if not calculated automatically)" }) + @ApiPropertyOptional({ + description: "Total transaction value (if not calculated automatically)", + }) @IsOptional() @IsNumber() totalValue?: number; @@ -116,7 +124,10 @@ export class UpdateTransactionDto { } export class TransactionFilterDto { - @ApiPropertyOptional({ enum: TransactionType, description: "Filter by transaction type" }) + @ApiPropertyOptional({ + enum: TransactionType, + description: "Filter by transaction type", + }) @IsOptional() @IsEnum(TransactionType) type?: TransactionType; @@ -126,7 +137,10 @@ export class TransactionFilterDto { @IsString() ticker?: string; - @ApiPropertyOptional({ enum: TransactionStatus, description: "Filter by transaction status" }) + @ApiPropertyOptional({ + enum: TransactionStatus, + description: "Filter by transaction status", + }) @IsOptional() @IsEnum(TransactionStatus) status?: TransactionStatus; @@ -253,7 +267,10 @@ export class TransactionHistoryResponseDto { @ApiProperty({ description: "Total pages" }) totalPages: number; - @ApiProperty({ type: [TransactionResponseDto], description: "Transactions in current page" }) + @ApiProperty({ + type: [TransactionResponseDto], + description: "Transactions in current page", + }) transactions: TransactionResponseDto[]; } @@ -293,9 +310,12 @@ export class TransactionExportDto { @ApiPropertyOptional({ description: "End date for export" }) endDate?: string; - @ApiPropertyOptional({ enum: TransactionType, description: "Filter by transaction type" }) + @ApiPropertyOptional({ + enum: TransactionType, + description: "Filter by transaction type", + }) type?: TransactionType; @ApiPropertyOptional({ description: "Include cost basis calculations" }) includeCostBasis?: boolean; -} \ No newline at end of file +} diff --git a/src/portfolio/entities/backtest-result.entity.ts b/src/portfolio/entities/backtest-result.entity.ts index 9aa5fd42..82c46ab3 100644 --- a/src/portfolio/entities/backtest-result.entity.ts +++ b/src/portfolio/entities/backtest-result.entity.ts @@ -8,7 +8,7 @@ import { JoinColumn, Index, } from "typeorm"; -import { User } from "../../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; export enum BacktestStatus { PENDING = "pending", diff --git a/src/portfolio/entities/portfolio.entity.ts b/src/portfolio/entities/portfolio.entity.ts index b71bc88e..dec88da6 100644 --- a/src/portfolio/entities/portfolio.entity.ts +++ b/src/portfolio/entities/portfolio.entity.ts @@ -10,7 +10,7 @@ import { OneToMany, JoinColumn, } from "typeorm"; -import { User } from "../../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; import { PortfolioAsset } from "./portfolio-asset.entity"; import { OptimizationHistory } from "./optimization-history.entity"; import { RebalancingEvent } from "./rebalancing-event.entity"; diff --git a/src/portfolio/entities/risk-profile.entity.ts b/src/portfolio/entities/risk-profile.entity.ts index 2b380992..e08ddb57 100644 --- a/src/portfolio/entities/risk-profile.entity.ts +++ b/src/portfolio/entities/risk-profile.entity.ts @@ -7,7 +7,7 @@ import { ManyToOne, JoinColumn, } from "typeorm"; -import { User } from "../../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; export enum RiskTolerance { VERY_CONSERVATIVE = "very_conservative", diff --git a/src/portfolio/entities/transaction.entity.ts b/src/portfolio/entities/transaction.entity.ts index fe34a2df..004ebab6 100644 --- a/src/portfolio/entities/transaction.entity.ts +++ b/src/portfolio/entities/transaction.entity.ts @@ -9,7 +9,7 @@ import { JoinColumn, } from "typeorm"; import { Portfolio } from "./portfolio.entity"; -import { User } from "../../user/entities/user.entity"; +import { User } from "src/user/entities/user.entity"; export enum TransactionType { BUY = "buy", diff --git a/src/portfolio/portfolio.controller.ts b/src/portfolio/portfolio.controller.ts index 3a0ba6b6..01701ed0 100644 --- a/src/portfolio/portfolio.controller.ts +++ b/src/portfolio/portfolio.controller.ts @@ -19,7 +19,10 @@ import { Throttle } from "@nestjs/throttler"; import { Response as ExpressResponse } from "express"; import { JwtAuthGuard } from "src/auth/jwt.guard"; import { PortfolioService } from "./services/portfolio.service"; -import { RebalancingService, RebalancingResult } from "./services/rebalancing.service"; +import { + RebalancingService, + RebalancingResult, +} from "./services/rebalancing.service"; import { PerformanceAnalyticsService } from "./services/performance-analytics.service"; import { BacktestingService } from "./services/backtesting.service"; import { MLPredictionService } from "./services/ml-prediction.service"; @@ -696,4 +699,4 @@ export class PortfolioController { ); return { message: "Transaction archived successfully" }; } -} \ No newline at end of file +} diff --git a/src/portfolio/services/portfolio.service.spec.ts b/src/portfolio/services/portfolio.service.spec.ts index 510aa052..c4f7f7ee 100644 --- a/src/portfolio/services/portfolio.service.spec.ts +++ b/src/portfolio/services/portfolio.service.spec.ts @@ -434,7 +434,8 @@ describe("PortfolioService (CRUD)", () => { mockAssetRepo.findOne.mockResolvedValueOnce(null); // Second add - simulate existing on ETH but not on POLYGON mockAssetRepo.findOne.mockImplementation(async (opts: any) => { - if (opts.where?.chain === Chain.ETHEREUM) return { id: "asset-1", ticker: "ETH" }; + if (opts.where?.chain === Chain.ETHEREUM) + return { id: "asset-1", ticker: "ETH" }; return null; }); @@ -458,15 +459,7 @@ describe("PortfolioService (CRUD)", () => { it("should reject invalid ticker symbol (too short)", async () => { await expect( - service.addAsset( - "pf-1", - "AB", - "Too Short", - 10, - 100, - 0, - Chain.ETHEREUM, - ), + service.addAsset("pf-1", "AB", "Too Short", 10, 100, 0, Chain.ETHEREUM), ).rejects.toThrow(BadRequestException); }); @@ -486,15 +479,7 @@ describe("PortfolioService (CRUD)", () => { it("should reject invalid ticker symbol (lowercase)", async () => { await expect( - service.addAsset( - "pf-1", - "btc", - "Bitcoin", - 10, - 100, - 0, - Chain.ETHEREUM, - ), + service.addAsset("pf-1", "btc", "Bitcoin", 10, 100, 0, Chain.ETHEREUM), ).rejects.toThrow(BadRequestException); }); @@ -514,15 +499,7 @@ describe("PortfolioService (CRUD)", () => { it("should reject negative quantity", async () => { await expect( - service.addAsset( - "pf-1", - "BTC", - "Bitcoin", - -1, - 100, - 0, - Chain.ETHEREUM, - ), + service.addAsset("pf-1", "BTC", "Bitcoin", -1, 100, 0, Chain.ETHEREUM), ).rejects.toThrow(BadRequestException); }); }); @@ -705,9 +682,9 @@ describe("PortfolioService (CRUD)", () => { it("should throw error if asset not found", async () => { mockAssetRepo.findOne.mockResolvedValue(null); - await expect( - service.removeAsset("pf-1", "non-existent"), - ).rejects.toThrow(BadRequestException); + await expect(service.removeAsset("pf-1", "non-existent")).rejects.toThrow( + BadRequestException, + ); }); }); diff --git a/src/portfolio/services/portfolio.service.ts b/src/portfolio/services/portfolio.service.ts index 3245c463..4a6a1bd8 100644 --- a/src/portfolio/services/portfolio.service.ts +++ b/src/portfolio/services/portfolio.service.ts @@ -126,8 +126,12 @@ export class PortfolioService { const page = query.page && query.page > 0 ? query.page : 1; const limit = query.limit && query.limit > 0 ? query.limit : 20; - const where: Record = { userId, status: Not(PortfolioStatus.ARCHIVED) }; - if (query.status && query.status !== PortfolioStatus.ARCHIVED) where.status = query.status; + const where: Record = { + userId, + status: Not(PortfolioStatus.ARCHIVED), + }; + if (query.status && query.status !== PortfolioStatus.ARCHIVED) + where.status = query.status; if (query.type) where.type = query.type; if (query.search) where.name = ILike(`%${query.search}%`); @@ -210,9 +214,7 @@ export class PortfolioService { /** * Validate allocation object contains non-negative numeric values. */ - private validateAllocation( - allocation: Record, - ): void { + private validateAllocation(allocation: Record): void { for (const [key, value] of Object.entries(allocation)) { if (typeof value !== "number" || value < 0) { throw new BadRequestException( @@ -338,7 +340,10 @@ export class PortfolioService { // Cost basis rebalancing for tax calculations // When adding to position, new cost basis = weighted average - if (updates.quantity > asset.quantity && updates.costBasis !== undefined) { + if ( + updates.quantity > asset.quantity && + updates.costBasis !== undefined + ) { const addedQty = updates.quantity - asset.quantity; const oldTotalCost = asset.quantity * (asset.costBasisPerShare || 0); const newTotalCost = addedQty * (updates.costBasis / addedQty); @@ -365,7 +370,8 @@ export class PortfolioService { // Recalculate value and unrealized gain/loss for tax tracking asset.value = asset.quantity * (asset.currentPrice || 0); asset.unrealizedGain = - asset.value - (asset.costBasis || asset.quantity * (asset.currentPrice || 0)); + asset.value - + (asset.costBasis || asset.quantity * (asset.currentPrice || 0)); const saved = await this.portfolioAssetRepository.save(asset); @@ -384,10 +390,7 @@ export class PortfolioService { * * Recalculates portfolio value and allocation after removal. */ - async removeAsset( - portfolioId: string, - assetId: string, - ): Promise { + async removeAsset(portfolioId: string, assetId: string): Promise { const asset = await this.portfolioAssetRepository.findOne({ where: { id: assetId, portfolioId }, }); diff --git a/src/portfolio/services/rebalancing.service.spec.ts b/src/portfolio/services/rebalancing.service.spec.ts index 779f8d4d..aafe5007 100644 --- a/src/portfolio/services/rebalancing.service.spec.ts +++ b/src/portfolio/services/rebalancing.service.spec.ts @@ -303,9 +303,9 @@ describe("RebalancingService", () => { it("throws BadRequestException if event not found", async () => { mockRebalancingRepo.findOne.mockResolvedValue(null); - await expect(service.approveRebalancing("missing")).rejects.toBeInstanceOf( - BadRequestException, - ); + await expect( + service.approveRebalancing("missing"), + ).rejects.toBeInstanceOf(BadRequestException); }); }); diff --git a/src/portfolio/services/rebalancing.service.ts b/src/portfolio/services/rebalancing.service.ts index ce1930f0..5e5c6f26 100644 --- a/src/portfolio/services/rebalancing.service.ts +++ b/src/portfolio/services/rebalancing.service.ts @@ -210,7 +210,8 @@ export class RebalancingService { const estimatedCost = this.calculateTransactionCosts(trades); const taxImpact = await this.calculateTaxImpact(portfolioId, trades); - const allocationAfter = portfolio.targetAllocation || portfolio.currentAllocation; + const allocationAfter = + portfolio.targetAllocation || portfolio.currentAllocation; if (dryRun) { return { @@ -406,4 +407,4 @@ export class RebalancingService { return 90; } } -} \ No newline at end of file +} diff --git a/src/portfolio/services/trading-transaction.service.spec.ts b/src/portfolio/services/trading-transaction.service.spec.ts index c69208bb..e6ec0f2d 100644 --- a/src/portfolio/services/trading-transaction.service.spec.ts +++ b/src/portfolio/services/trading-transaction.service.spec.ts @@ -2,7 +2,11 @@ import { Test, TestingModule } from "@nestjs/testing"; import { getRepositoryToken } from "@nestjs/typeorm"; import { DataSource, Repository } from "typeorm"; import { TradingTransactionService } from "./trading-transaction.service"; -import { Transaction, TransactionType, TransactionStatus } from "../entities/transaction.entity"; +import { + Transaction, + TransactionType, + TransactionStatus, +} from "../entities/transaction.entity"; import { PortfolioAsset } from "../entities/portfolio-asset.entity"; import { Portfolio } from "../entities/portfolio.entity"; import { @@ -120,9 +124,11 @@ describe("TradingTransactionService", () => { }, }; - jest.spyOn(dataSource, "transaction").mockImplementation(async (_, cb) => { - return cb(mockManager as any); - }); + jest + .spyOn(dataSource, "transaction") + .mockImplementation(async (_, cb) => { + return cb(mockManager as any); + }); const result = await service.executeTrade({ portfolioId: "portfolio-1", @@ -176,14 +182,18 @@ describe("TradingTransactionService", () => { }, }; - jest.spyOn(dataSource, "transaction").mockImplementation(async (_, cb) => { - return cb(mockManager as any); - }); + jest + .spyOn(dataSource, "transaction") + .mockImplementation(async (_, cb) => { + return cb(mockManager as any); + }); await service.executeTrade(operation); // Try to process the same key again - await expect(service.executeTrade(operation)).rejects.toThrow(ConflictException); + await expect(service.executeTrade(operation)).rejects.toThrow( + ConflictException, + ); }); it("should throw PortfolioNotFoundException if portfolio not found", async () => { @@ -201,9 +211,11 @@ describe("TradingTransactionService", () => { }, }; - jest.spyOn(dataSource, "transaction").mockImplementation(async (_, cb) => { - return cb(mockManager as any); - }); + jest + .spyOn(dataSource, "transaction") + .mockImplementation(async (_, cb) => { + return cb(mockManager as any); + }); await expect( service.executeTrade({ @@ -242,9 +254,11 @@ describe("TradingTransactionService", () => { }, }; - jest.spyOn(dataSource, "transaction").mockImplementation(async (_, cb) => { - return cb(mockManager as any); - }); + jest + .spyOn(dataSource, "transaction") + .mockImplementation(async (_, cb) => { + return cb(mockManager as any); + }); await expect( service.executeTrade({ @@ -279,7 +293,9 @@ describe("TradingTransactionService", () => { }); it("should throw ConflictException if idempotency key already exists", async () => { - jest.spyOn(transactionRepository, "findOne").mockResolvedValue(mockTransaction()); + jest + .spyOn(transactionRepository, "findOne") + .mockResolvedValue(mockTransaction()); await expect( service.recordTransaction("portfolio-1", "user-1", { diff --git a/src/portfolio/services/trading-transaction.service.ts b/src/portfolio/services/trading-transaction.service.ts index ea9c9d37..59f90b0d 100644 --- a/src/portfolio/services/trading-transaction.service.ts +++ b/src/portfolio/services/trading-transaction.service.ts @@ -1,4 +1,9 @@ -import { Injectable, Logger, ConflictException, BadRequestException } from "@nestjs/common"; +import { + Injectable, + Logger, + ConflictException, + BadRequestException, +} from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { @@ -9,7 +14,11 @@ import { DataSource, EntityManager } from "typeorm"; import BigNumber from "bignumber.js"; import { Portfolio } from "../entities/portfolio.entity"; import { PortfolioAsset } from "../entities/portfolio-asset.entity"; -import { Transaction, TransactionType, TransactionStatus } from "../entities/transaction.entity"; +import { + Transaction, + TransactionType, + TransactionStatus, +} from "../entities/transaction.entity"; import { CreateTransactionDto } from "../dto/transaction.dto"; // Configure BigNumber for financial precision (no exponential notation, 18 dp) @@ -113,7 +122,9 @@ export class TradingTransactionService { const saved = await manager.getRepository(PortfolioAsset).save(asset); // Record transaction - const transactionType = bnQuantity.isPositive() ? TransactionType.BUY : TransactionType.SELL; + const transactionType = bnQuantity.isPositive() + ? TransactionType.BUY + : TransactionType.SELL; const transaction = manager.getRepository(Transaction).create({ portfolioId: op.portfolioId, userId: op.userId, @@ -194,7 +205,9 @@ export class TradingTransactionService { exchange: dto.exchange, notes: dto.notes, costBasisPerUnit: dto.costBasisPerUnit || dto.price, - transactionDate: dto.transactionDate ? new Date(dto.transactionDate) : new Date(), + transactionDate: dto.transactionDate + ? new Date(dto.transactionDate) + : new Date(), idempotencyKey: dto.idempotencyKey, metadata: dto.metadata, }); @@ -222,7 +235,9 @@ export class TradingTransactionService { (dto.type === TransactionType.BUY || dto.type === TransactionType.SELL) && (!dto.price || dto.price <= 0) ) { - throw new BadRequestException("Price must be positive for buy/sell transactions"); + throw new BadRequestException( + "Price must be positive for buy/sell transactions", + ); } // Validate fees are non-negative diff --git a/src/portfolio/services/transaction-history.service.spec.ts b/src/portfolio/services/transaction-history.service.spec.ts index 96992e68..2bd5d610 100644 --- a/src/portfolio/services/transaction-history.service.spec.ts +++ b/src/portfolio/services/transaction-history.service.spec.ts @@ -2,17 +2,22 @@ import { Test, TestingModule } from "@nestjs/testing"; import { getRepositoryToken } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { TransactionHistoryService } from "./transaction-history.service"; -import { Transaction, TransactionType, TransactionStatus } from "../entities/transaction.entity"; -import { CreateTransactionDto, TransactionFilterDto } from "../dto/transaction.dto"; +import { + Transaction, + TransactionType, + TransactionStatus, +} from "../entities/transaction.entity"; +import { + CreateTransactionDto, + TransactionFilterDto, +} from "../dto/transaction.dto"; import { NotFoundException, BadRequestException } from "@nestjs/common"; describe("TransactionHistoryService", () => { let service: TransactionHistoryService; let repository: Repository; - const mockTransaction = ( - overrides?: Partial, - ): Transaction => { + const mockTransaction = (overrides?: Partial): Transaction => { const tx = new Transaction(); tx.id = "tx-1"; tx.portfolioId = "portfolio-1"; @@ -49,7 +54,9 @@ describe("TransactionHistoryService", () => { }).compile(); service = module.get(TransactionHistoryService); - repository = module.get>(getRepositoryToken(Transaction)); + repository = module.get>( + getRepositoryToken(Transaction), + ); }); describe("getTransactionHistory", () => { @@ -66,7 +73,9 @@ describe("TransactionHistoryService", () => { getMany: jest.fn().mockResolvedValue(transactions), }; - jest.spyOn(repository, "createQueryBuilder").mockReturnValue(queryBuilder as any); + jest + .spyOn(repository, "createQueryBuilder") + .mockReturnValue(queryBuilder as any); const result = await service.getTransactionHistory( "portfolio-1", @@ -93,7 +102,9 @@ describe("TransactionHistoryService", () => { getMany: jest.fn().mockResolvedValue(transactions), }; - jest.spyOn(repository, "createQueryBuilder").mockReturnValue(queryBuilder as any); + jest + .spyOn(repository, "createQueryBuilder") + .mockReturnValue(queryBuilder as any); const result = await service.getTransactionHistory( "portfolio-1", @@ -117,7 +128,9 @@ describe("TransactionHistoryService", () => { getMany: jest.fn().mockResolvedValue(transactions), }; - jest.spyOn(repository, "createQueryBuilder").mockReturnValue(queryBuilder as any); + jest + .spyOn(repository, "createQueryBuilder") + .mockReturnValue(queryBuilder as any); const result = await service.getTransactionHistory( "portfolio-1", @@ -141,7 +154,9 @@ describe("TransactionHistoryService", () => { getMany: jest.fn().mockResolvedValue(transactions), }; - jest.spyOn(repository, "createQueryBuilder").mockReturnValue(queryBuilder as any); + jest + .spyOn(repository, "createQueryBuilder") + .mockReturnValue(queryBuilder as any); const result = await service.getTransactionHistory( "portfolio-1", @@ -167,7 +182,9 @@ describe("TransactionHistoryService", () => { getMany: jest.fn().mockResolvedValue([]), }; - jest.spyOn(repository, "createQueryBuilder").mockReturnValue(queryBuilder as any); + jest + .spyOn(repository, "createQueryBuilder") + .mockReturnValue(queryBuilder as any); await expect( service.getTransactionHistory("portfolio-1", "user-1", { @@ -183,7 +200,11 @@ describe("TransactionHistoryService", () => { const tx = mockTransaction(); jest.spyOn(repository, "findOne").mockResolvedValue(tx); - const result = await service.getTransaction("tx-1", "portfolio-1", "user-1"); + const result = await service.getTransaction( + "tx-1", + "portfolio-1", + "user-1", + ); expect(result.id).toBe("tx-1"); expect(repository.findOne).toHaveBeenCalledWith({ @@ -229,7 +250,9 @@ describe("TransactionHistoryService", () => { getMany: jest.fn().mockResolvedValue([tx1, tx2]), }; - jest.spyOn(repository, "createQueryBuilder").mockReturnValue(queryBuilder as any); + jest + .spyOn(repository, "createQueryBuilder") + .mockReturnValue(queryBuilder as any); const result = await service.calculateCostBasis( "portfolio-1", @@ -250,7 +273,9 @@ describe("TransactionHistoryService", () => { getMany: jest.fn().mockResolvedValue([]), }; - jest.spyOn(repository, "createQueryBuilder").mockReturnValue(queryBuilder as any); + jest + .spyOn(repository, "createQueryBuilder") + .mockReturnValue(queryBuilder as any); await expect( service.calculateCostBasis("portfolio-1", "user-1", "AAPL"), @@ -272,7 +297,9 @@ describe("TransactionHistoryService", () => { getMany: jest.fn().mockResolvedValue(transactions), }; - jest.spyOn(repository, "createQueryBuilder").mockReturnValue(queryBuilder as any); + jest + .spyOn(repository, "createQueryBuilder") + .mockReturnValue(queryBuilder as any); const csv = await service.exportTransactionsAsCSV( "portfolio-1", @@ -297,7 +324,9 @@ describe("TransactionHistoryService", () => { getMany: jest.fn().mockResolvedValue([]), }; - jest.spyOn(repository, "createQueryBuilder").mockReturnValue(queryBuilder as any); + jest + .spyOn(repository, "createQueryBuilder") + .mockReturnValue(queryBuilder as any); const result = await service.exportTransactionsAsCSV( "portfolio-1", @@ -323,7 +352,9 @@ describe("TransactionHistoryService", () => { getMany: jest.fn().mockResolvedValue(transactions), }; - jest.spyOn(repository, "createQueryBuilder").mockReturnValue(queryBuilder as any); + jest + .spyOn(repository, "createQueryBuilder") + .mockReturnValue(queryBuilder as any); const result = await service.exportTransactionsAsJSON( "portfolio-1", diff --git a/src/portfolio/services/transaction-history.service.ts b/src/portfolio/services/transaction-history.service.ts index e04fbe78..235c5302 100644 --- a/src/portfolio/services/transaction-history.service.ts +++ b/src/portfolio/services/transaction-history.service.ts @@ -7,7 +7,11 @@ import { import { InjectRepository } from "@nestjs/typeorm"; import { Repository, Between, Not, IsNull, SelectQueryBuilder } from "typeorm"; import BigNumber from "bignumber.js"; -import { Transaction, TransactionType, TransactionStatus } from "../entities/transaction.entity"; +import { + Transaction, + TransactionType, + TransactionStatus, +} from "../entities/transaction.entity"; import { TransactionFilterDto, TransactionResponseDto, @@ -118,8 +122,14 @@ export class TransactionHistoryService { const total = await query.getCount(); // Sort and paginate - query.orderBy("transaction.transactionDate", sortBy === "asc" ? "ASC" : "DESC"); - query.addOrderBy("transaction.createdAt", sortBy === "asc" ? "ASC" : "DESC"); + query.orderBy( + "transaction.transactionDate", + sortBy === "asc" ? "ASC" : "DESC", + ); + query.addOrderBy( + "transaction.createdAt", + sortBy === "asc" ? "ASC" : "DESC", + ); const skip = (validPage - 1) * validLimit; query.skip(skip).take(validLimit); @@ -133,7 +143,9 @@ export class TransactionHistoryService { page: validPage, limit: validLimit, totalPages, - transactions: transactions.map((t) => TransactionResponseDto.fromEntity(t)), + transactions: transactions.map((t) => + TransactionResponseDto.fromEntity(t), + ), }; } @@ -203,7 +215,10 @@ export class TransactionHistoryService { const txPrice = new BigNumber(tx.price || 0); const txFees = new BigNumber(tx.fees || 0); - if (tx.type === TransactionType.BUY || tx.type === TransactionType.STAKE) { + if ( + tx.type === TransactionType.BUY || + tx.type === TransactionType.STAKE + ) { // Add to position const costPerUnit = txPrice.plus(txFees.dividedBy(txQuantity.abs())); const transactionCost = txQuantity.multipliedBy(costPerUnit); @@ -262,7 +277,9 @@ export class TransactionHistoryService { .where("transaction.portfolioId = :portfolioId", { portfolioId }) .andWhere("transaction.userId = :userId", { userId }) .andWhere("transaction.archivedAt IS NULL") - .andWhere("transaction.status = :status", { status: TransactionStatus.COMPLETED }) + .andWhere("transaction.status = :status", { + status: TransactionStatus.COMPLETED, + }) .getRawMany(); const tickers = tickersResult.map((r: any) => r.ticker); @@ -397,7 +414,9 @@ export class TransactionHistoryService { }); if (!transaction) { - throw new NotFoundException(`Transaction ${transactionId} not found or already archived`); + throw new NotFoundException( + `Transaction ${transactionId} not found or already archived`, + ); } transaction.archivedAt = new Date(); @@ -462,9 +481,15 @@ export class TransactionHistoryService { byStatus[tx.status] = (byStatus[tx.status] || 0) + 1; const value = new BigNumber(tx.totalValue || 0); - if (tx.type === TransactionType.BUY || tx.type === TransactionType.DEPOSIT) { + if ( + tx.type === TransactionType.BUY || + tx.type === TransactionType.DEPOSIT + ) { totalBuys = totalBuys.plus(value); - } else if (tx.type === TransactionType.SELL || tx.type === TransactionType.WITHDRAWAL) { + } else if ( + tx.type === TransactionType.SELL || + tx.type === TransactionType.WITHDRAWAL + ) { totalSells = totalSells.plus(value); } } diff --git a/src/routes/health.ts b/src/routes/health.ts index eb3f9416..96de8f40 100644 --- a/src/routes/health.ts +++ b/src/routes/health.ts @@ -1,36 +1,36 @@ -import { Router } from 'express'; -import db from '../services/database'; -import cache from '../services/cache'; -import externalService from '../services/external'; +import { Router } from "express"; +import db from "../services/database"; +import cache from "../services/cache"; +import externalService from "../services/external"; const router = Router(); // Liveness probe – always returns OK if the app is running -router.get('/health/live', (_req, res) => { - res.status(200).json({ status: 'ok', message: 'Live' }); +router.get("/health/live", (_req, res) => { + res.status(200).json({ status: "ok", message: "Live" }); }); // Readiness probe – checks DB, cache, and external services -router.get('/health/ready', async (_req, res) => { +router.get("/health/ready", async (_req, res) => { try { await db.ping(); // simple DB ping await cache.ping(); // cache ping await externalService.ping(); // external service check - res.status(200).json({ status: 'ok', message: 'Ready' }); + res.status(200).json({ status: "ok", message: "Ready" }); } catch (err) { - res.status(503).json({ status: 'fail', error: (err as Error).message }); + res.status(503).json({ status: "fail", error: (err as Error).message }); } }); // Startup probe – runs on start-up to ensure all dependencies initialize correctly -router.get('/health/startup', async (_req, res) => { +router.get("/health/startup", async (_req, res) => { try { await db.initialize(); await cache.initialize(); await externalService.initialize(); - res.status(200).json({ status: 'ok', message: 'Startup checks passed' }); + res.status(200).json({ status: "ok", message: "Startup checks passed" }); } catch (err) { - res.status(500).json({ status: 'error', error: (err as Error).message }); + res.status(500).json({ status: "error", error: (err as Error).message }); } }); diff --git a/src/user/entities/user.entity.ts b/src/user/entities/user.entity.ts index 16c5c93f..a5c520b9 100644 --- a/src/user/entities/user.entity.ts +++ b/src/user/entities/user.entity.ts @@ -9,8 +9,8 @@ import { ManyToOne, JoinColumn, } from "typeorm"; -import { ProvenanceRecord } from "../../audit/entities/provenance-record.entity"; -import { Wallet } from "../../auth/entities/wallet.entity"; +import { ProvenanceRecord } from "src/audit/entities/provenance-record.entity"; +import { Wallet } from "src/auth/entities/wallet.entity"; export enum UserRole { USER = "user", diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 0dab9fd9..615e7fdd 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -9,7 +9,7 @@ import { User } from "./entities/user.entity"; import { CreateUserDto } from "./dto/create-user.dto"; import { UpdateUserDto } from "./dto/update-user.dto"; import { UserRole } from "./entities/user.entity"; -import { createSpan } from "../config/tracing"; +import { createSpan } from "src/config/tracing"; /** Pairs of roles that are mutually exclusive */ const CONFLICTING_ROLE_PAIRS: [UserRole, UserRole][] = [ @@ -51,27 +51,35 @@ export class UserService { */ async assignRole(userId: string, newRole: UserRole): Promise { // Example of manual span creation with OpenTelemetry - return createSpan("user.assign-role", async (span) => { - span.setAttribute("user.id", userId); - span.setAttribute("user.role.new", newRole); - - const user = await this.findOne(userId); - if (!user) { - throw new NotFoundException(`User ${userId} not found`); - } + return createSpan( + "user.assign-role", + async (span) => { + span.setAttribute("user.id", userId); + span.setAttribute("user.role.new", newRole); - // Child span for role validation - return createSpan("user.validate-role-conflict", async (childSpan) => { - childSpan.setAttribute("user.role.current", user.role); - this.assertNoRoleConflict(user.role, newRole); - - user.role = newRole; - const savedUser = await this.userRepository.save(user); - - span.setAttribute("success", true); - return savedUser; - }, { "validation.type": "role-conflict" }); - }, { "module": "user-service", "operation": "role-assignment" }); + const user = await this.findOne(userId); + if (!user) { + throw new NotFoundException(`User ${userId} not found`); + } + + // Child span for role validation + return createSpan( + "user.validate-role-conflict", + async (childSpan) => { + childSpan.setAttribute("user.role.current", user.role); + this.assertNoRoleConflict(user.role, newRole); + + user.role = newRole; + const savedUser = await this.userRepository.save(user); + + span.setAttribute("success", true); + return savedUser; + }, + { "validation.type": "role-conflict" }, + ); + }, + { module: "user-service", operation: "role-assignment" }, + ); } /** @@ -94,4 +102,4 @@ export class UserService { ); } } -} \ No newline at end of file +} diff --git a/test/portfolio/transactions.e2e-spec.ts b/test/portfolio/transactions.e2e-spec.ts index 57254a95..a5ea9221 100644 --- a/test/portfolio/transactions.e2e-spec.ts +++ b/test/portfolio/transactions.e2e-spec.ts @@ -2,9 +2,17 @@ import { Test, TestingModule } from "@nestjs/testing"; import { INestApplication, ValidationPipe } from "@nestjs/common"; import * as request from "supertest"; import { TypeOrmModule } from "@nestjs/typeorm"; -import { Portfolio, PortfolioStatus, PortfolioType } from "../entities/portfolio.entity"; +import { + Portfolio, + PortfolioStatus, + PortfolioType, +} from "../entities/portfolio.entity"; import { PortfolioAsset, AssetType } from "../entities/portfolio-asset.entity"; -import { Transaction, TransactionType, TransactionStatus } from "../entities/transaction.entity"; +import { + Transaction, + TransactionType, + TransactionStatus, +} from "../entities/transaction.entity"; import { PortfolioModule } from "../portfolio.module"; import { User } from "../../user/entities/user.entity"; @@ -139,7 +147,9 @@ describe("Portfolio Transactions Integration (e2e)", () => { describe("Transaction History and Filtering", () => { it("should retrieve transaction history with pagination", async () => { const response = await request(app.getHttpServer()) - .get(`/portfolio/portfolios/${portfolioId}/transactions?page=1&limit=20`) + .get( + `/portfolio/portfolios/${portfolioId}/transactions?page=1&limit=20`, + ) .set("Authorization", `Bearer ${userId}`); expect(response.status).toBe(200); @@ -158,9 +168,11 @@ describe("Portfolio Transactions Integration (e2e)", () => { .set("Authorization", `Bearer ${userId}`); expect(response.status).toBe(200); - expect(response.body.transactions.every((t: any) => t.type === TransactionType.BUY)).toBe( - true, - ); + expect( + response.body.transactions.every( + (t: any) => t.type === TransactionType.BUY, + ), + ).toBe(true); }); it("should filter transactions by ticker", async () => { @@ -169,11 +181,15 @@ describe("Portfolio Transactions Integration (e2e)", () => { .set("Authorization", `Bearer ${userId}`); expect(response.status).toBe(200); - expect(response.body.transactions.every((t: any) => t.ticker === "AAPL")).toBe(true); + expect( + response.body.transactions.every((t: any) => t.ticker === "AAPL"), + ).toBe(true); }); it("should filter transactions by date range", async () => { - const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(); + const startDate = new Date( + Date.now() - 30 * 24 * 60 * 60 * 1000, + ).toISOString(); const endDate = new Date().toISOString(); const response = await request(app.getHttpServer()) @@ -195,7 +211,9 @@ describe("Portfolio Transactions Integration (e2e)", () => { const transactionId = listResponse.body.transactions[0].id; const response = await request(app.getHttpServer()) - .get(`/portfolio/portfolios/${portfolioId}/transactions/${transactionId}`) + .get( + `/portfolio/portfolios/${portfolioId}/transactions/${transactionId}`, + ) .set("Authorization", `Bearer ${userId}`); expect(response.status).toBe(200); @@ -206,7 +224,9 @@ describe("Portfolio Transactions Integration (e2e)", () => { describe("Cost Basis Calculation", () => { it("should calculate cost basis for a specific ticker", async () => { const response = await request(app.getHttpServer()) - .get(`/portfolio/portfolios/${portfolioId}/transactions/cost-basis/AAPL`) + .get( + `/portfolio/portfolios/${portfolioId}/transactions/cost-basis/AAPL`, + ) .set("Authorization", `Bearer ${userId}`); expect(response.status).toBe(200); @@ -312,7 +332,9 @@ describe("Portfolio Transactions Integration (e2e)", () => { // Should include archived when flag is set const withArchived = await request(app.getHttpServer()) - .get(`/portfolio/portfolios/${portfolioId}/transactions?includeArchived=true`) + .get( + `/portfolio/portfolios/${portfolioId}/transactions?includeArchived=true`, + ) .set("Authorization", `Bearer ${userId}`); expect(withArchived.body.total).toBeGreaterThanOrEqual(beforeCount); diff --git a/test/tracing.e2e-spec.ts b/test/tracing.e2e-spec.ts index 089eb622..c7906c5a 100644 --- a/test/tracing.e2e-spec.ts +++ b/test/tracing.e2e-spec.ts @@ -11,7 +11,7 @@ describe("Tracing (e2e)", () => { beforeAll(async () => { // Start tracing before tests await startTracing(); - + const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); @@ -28,14 +28,14 @@ describe("Tracing (e2e)", () => { it("should generate traces for HTTP requests", async () => { // Create a manual span to test tracing functionality const tracer = trace.getTracer("test-tracer"); - + await tracer.startActiveSpan("test-request-span", async (span: Span) => { try { // Make a request to health endpoint which should generate its own spans const response = await request(app.getHttpServer()) .get("/api/v1/health") .expect(200); - + expect(response.body).toBeDefined(); span.setAttribute("http.status_code", response.statusCode); span.setAttribute("success", true); @@ -51,13 +51,15 @@ describe("Tracing (e2e)", () => { it("should include trace ID in logs", async () => { const { createLogger, logger } = require("../src/config/logger"); const testLogger = createLogger({ test: "tracing-test" }); - + // Log something - trace ID should be automatically added if available - await trace.getTracer("test-logger").startActiveSpan("log-test-span", async (span) => { - testLogger.info("Test log with trace context"); - span.end(); - }); - + await trace + .getTracer("test-logger") + .startActiveSpan("log-test-span", async (span) => { + testLogger.info("Test log with trace context"); + span.end(); + }); + expect(true).toBe(true); // If we got here, logging with trace context works }); @@ -65,13 +67,13 @@ describe("Tracing (e2e)", () => { const spanProcessors = (sdk as any)._spanProcessors; expect(spanProcessors).toBeDefined(); expect(spanProcessors.length).toBeGreaterThan(0); - + // At least one exporter should be configured (Jaeger by default) - const hasJaegerExporter = spanProcessors.some(processor => { + const hasJaegerExporter = spanProcessors.some((processor) => { const exporter = (processor as any)._exporter; return exporter && exporter.constructor.name === "JaegerExporter"; }); - + expect(hasJaegerExporter).toBe(true); }); @@ -80,4 +82,4 @@ describe("Tracing (e2e)", () => { expect(sampler).toBeDefined(); expect(sampler.constructor.name).toBe("AdaptiveSampler"); }); -}); \ No newline at end of file +}); diff --git a/test/tracing.sampling.spec.ts b/test/tracing.sampling.spec.ts index ee4a9dc0..c18b4e0c 100644 --- a/test/tracing.sampling.spec.ts +++ b/test/tracing.sampling.spec.ts @@ -18,16 +18,18 @@ describe("Tracing Sampling Validation", () => { it("should have traceIdRatio sampler configured with correct parameters", () => { expect(sampler).toBeDefined(); expect(sampler.constructor.name).toBe("TraceIdRatioBasedSampler"); - + // Verify sampler has the ratio property expect(sampler._ratio).toBeDefined(); - console.log(`TraceIdRatio sampler configured with ratio: ${sampler._ratio}`); + console.log( + `TraceIdRatio sampler configured with ratio: ${sampler._ratio}`, + ); }); it("should sample traces within configured rate limits", () => { const sampleResults: boolean[] = []; const traceId = "abcdef1234567890abcdef1234567890"; - + // Generate multiple sampling decisions for (let i = 0; i < 100; i++) { const spanContext: SpanContext = { @@ -35,7 +37,7 @@ describe("Tracing Sampling Validation", () => { spanId: "1234567890abcdef", traceFlags: TraceFlags.NONE, }; - + try { const result: SamplingResult = sampler.shouldSample( undefined, // parentContext @@ -43,44 +45,48 @@ describe("Tracing Sampling Validation", () => { `test-span-${i}`, undefined, // spanKind {}, // attributes - [] // links + [], // links ); - + sampleResults.push(result.decision === 1); // RECORD_AND_SAMPLED } catch (e) { // AdaptiveSampler's shouldSample might have different signature, this is fine continue; } } - - console.log(`Sampling decisions: ${sampleResults.filter(Boolean).length}/${sampleResults.length} traces sampled`); + + console.log( + `Sampling decisions: ${sampleResults.filter(Boolean).length}/${sampleResults.length} traces sampled`, + ); }); it("should respect minimum sampling rate under high load", () => { // Verify the configuration maintains minimum sampling rate const minRate = parseFloat(process.env.OTEL_MIN_SAMPLING_RATE || "0.1"); const maxRate = parseFloat(process.env.OTEL_MAX_SAMPLING_RATE || "1.0"); - + expect(minRate).toBeGreaterThan(0); expect(maxRate).toBeLessThanOrEqual(1); expect(minRate).toBeLessThan(maxRate); - - console.log(`Sampling rate bounds configured: min=${minRate}, max=${maxRate}`); + + console.log( + `Sampling rate bounds configured: min=${minRate}, max=${maxRate}`, + ); }); it("should propagate trace flags correctly for sampled traces", () => { const tracer = trace.getTracer("sampling-test"); - + // Create a span and verify it gets sampled properly return tracer.startActiveSpan("test-sampled-span", async (span) => { const spanContext = span.spanContext(); - + // If the span is sampled, trace flags should include SAMPLED flag if (spanContext.traceFlags & TraceFlags.SAMPLED) { expect(true).toBe(true); // Span was sampled } - + span.end(); }); }); -}); \ No newline at end of file +}); diff --git a/tsconfig.json b/tsconfig.json index e08f6339..bd201418 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,8 +27,11 @@ "noUnusedLocals": false, "noUnusedParameters": false, "noImplicitReturns": false, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": false, + "paths": { + "src/*": ["src/*"] + } }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "test"] -} +} \ No newline at end of file