diff --git a/docs/treasury-api.md b/docs/treasury-api.md new file mode 100644 index 0000000..520d801 --- /dev/null +++ b/docs/treasury-api.md @@ -0,0 +1,35 @@ +# Treasury API Documentation + +## Overview +The Treasury module provides several endpoints for managing financial operations within BudgetChain. + +### Endpoints + +**GET /treasury** +- Description: Retrieve all treasury entities +- Response: An array of treasury details + +**GET /treasury/:id** +- Description: Retrieve treasury by ID +- Parameters: + - id (string): Treasury ID +- Response: Treasury entity details + +**POST /treasury** +- Description: Create a new treasury entity +- Parameters: + - name (string): Name of the treasury + - initialFund (number): Initial funding amount +- Response: The created treasury entity + +**PUT /treasury/:id** +- Description: Update a treasury entity +- Parameters: + - id (string): Treasury ID +- Response: Updated treasury entity + +**DELETE /treasury/:id** +- Description: Remove a treasury entity +- Parameters: + - id (string): Treasury ID +- Response: Confirmation of deletion \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 368a225..47f9e73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/event-emitter": "^3.0.1", "@nestjs/jwt": "^11.0.0", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", @@ -2613,6 +2614,18 @@ } } }, + "node_modules/@nestjs/event-emitter": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-3.0.1.tgz", + "integrity": "sha512-0Ln/x+7xkU6AJFOcQI9tIhUMXVF7D5itiaQGOyJbXtlAfAIt8gzDdJm+Im7cFzKoWkiW5nCXCPh6GSvdQd/3Dw==", + "dependencies": { + "eventemitter2": "6.4.9" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, "node_modules/@nestjs/jwt": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.0.tgz", @@ -6934,6 +6947,11 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", diff --git a/package.json b/package.json index f7b7bf8..e8019a5 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/event-emitter": "^3.0.1", "@nestjs/jwt": "^11.0.0", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", diff --git a/src/main.ts b/src/main.ts index ca16d44..c3608a6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,11 +4,15 @@ import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.enableCors(); app.useGlobalPipes( new ValidationPipe({ whitelist: true, - forbidNonWhitelisted: true, + forbidNonWhitelisted: false, transform: true, + transformOptions: { + enableImplicitConversion: true, + }, }) ); await app.listen(process.env.APP_PORT ?? 3000); diff --git a/src/migrations/1745944410893-CreateUsersTable.ts b/src/migrations/1745944410893-CreateUsersTable.ts new file mode 100644 index 0000000..fc9ebe1 --- /dev/null +++ b/src/migrations/1745944410893-CreateUsersTable.ts @@ -0,0 +1,35 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +export class CreateUsersTable1745944410893 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // First create the enum types + await queryRunner.query(` + CREATE TYPE "public"."user_role_enum" AS ENUM('user', 'admin'); + CREATE TYPE "public"."auth_provider_enum" AS ENUM('local', 'starknet'); + `); + // Then create the users table + await queryRunner.query(` + CREATE TABLE "users" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "email" character varying NOT NULL, + "password" character varying, + "starknetWalletAddress" character varying, + "role" "public"."user_role_enum" NOT NULL DEFAULT 'user', + "provider" "public"."auth_provider_enum" NOT NULL DEFAULT 'local', + "isActive" boolean NOT NULL DEFAULT true, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "UQ_users_email" UNIQUE ("email"), + CONSTRAINT "PK_users" PRIMARY KEY ("id") + ); + `); + } + public async down(queryRunner: QueryRunner): Promise { + // Drop the table first + await queryRunner.query(`DROP TABLE "users"`); + // Then drop the enum types + await queryRunner.query(` + DROP TYPE "public"."user_role_enum"; + DROP TYPE "public"."auth_provider_enum"; + `); + } +} diff --git a/src/modules/auth/controllers/auth.controller.ts b/src/modules/auth/controllers/auth.controller.ts deleted file mode 100644 index 676938c..0000000 --- a/src/modules/auth/controllers/auth.controller.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - Controller, - Post, - Body, - HttpCode, - HttpStatus, - UnauthorizedException, -} from '@nestjs/common'; -import { AuthService } from '../services/auth.service'; -import { LoginDto } from '../dto/login.dto'; -import { WalletLoginDto } from '../dto/wallet-login.dto'; - -@Controller('auth') -export class AuthController { - constructor(private authService: AuthService) {} - - @HttpCode(HttpStatus.OK) - @Post('login') - async login(@Body() loginDto: LoginDto): Promise<{ token: string }> { - const user = await this.authService.validateUser( - loginDto.email, - loginDto.password - ); - if (!user) { - throw new UnauthorizedException('Invalid credentials'); - } - const token = this.authService.generateToken(user); - return { token }; - } - - @HttpCode(HttpStatus.OK) - @Post('wallet-login') - async walletLogin( - @Body() walletLoginDto: WalletLoginDto - ): Promise<{ token: string }> { - const user = await this.authService.validateWallet( - walletLoginDto.walletAddress - ); - if (!user) { - throw new UnauthorizedException('Invalid wallet address'); - } - const token = this.authService.generateToken(user); - return { token }; - } -} diff --git a/src/modules/treasury/controllers/treasury.controller.ts b/src/modules/treasury/controllers/treasury.controller.ts index 743f8af..3048ffe 100644 --- a/src/modules/treasury/controllers/treasury.controller.ts +++ b/src/modules/treasury/controllers/treasury.controller.ts @@ -1,17 +1,82 @@ -import { Controller, Get, Post, Body, Param, Query } from '@nestjs/common'; +import { + Controller, + Get, + Post, + Body, + Param, + Query, + Put, + Delete, +} from '@nestjs/common'; import { TreasuryService } from '../services/treasury.service'; import { Roles } from '../../../shared/decorators/roles.decorator'; import { UserRole } from '../../user/entities/user.entity'; +import { CreateTreasuryDto } from '../dto/create-treasury.dto'; +import { UpdateTreasuryDto } from '../dto/update-treasury.dto'; +import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger'; +@ApiTags('Treasuries') @Controller('treasury') export class TreasuryController { constructor(private readonly treasuryService: TreasuryService) {} + /** + * Create a new treasury + */ + @Post() + @Roles(UserRole.ADMIN) + @ApiOperation({ summary: 'Create a new treasury' }) + @ApiBody({ type: CreateTreasuryDto }) + @ApiResponse({ status: 201, description: 'Treasury created' }) + async createTreasury(@Body() dto: CreateTreasuryDto) { + return this.treasuryService.create(dto); + } + + /** + * Get treasury by ID + */ + @Get(':id') + @Roles(UserRole.ADMIN, UserRole.TREASURER) + @ApiOperation({ summary: 'Get treasury by ID' }) + @ApiResponse({ status: 200, description: 'Treasury found' }) + async getTreasuryById(@Param('id') id: string) { + return this.treasuryService.findOne(id); + } + + /** + * Update treasury by ID + */ + @Put(':id') + @Roles(UserRole.ADMIN) + @ApiOperation({ summary: 'Update treasury by ID' }) + @ApiBody({ type: UpdateTreasuryDto }) + @ApiResponse({ status: 200, description: 'Treasury updated' }) + async updateTreasury( + @Param('id') id: string, + @Body() dto: UpdateTreasuryDto + ) { + return this.treasuryService.update(id, dto); + } + + /** + * Delete treasury by ID + */ + @Delete(':id') + @Roles(UserRole.ADMIN) + @ApiOperation({ summary: 'Delete treasury by ID' }) + @ApiResponse({ status: 204, description: 'Treasury deleted' }) + async deleteTreasury(@Param('id') id: string) { + await this.treasuryService.delete(id); + return; + } + /** * Get treasury overview with balances, allocations, and recent activity */ @Get('overview') @Roles(UserRole.ADMIN, UserRole.TREASURER) + @ApiOperation({ summary: 'Get treasury overview' }) + @ApiResponse({ status: 200, description: 'Treasury overview retrieved' }) async getTreasuryOverview() { return this.treasuryService.getTreasuryOverview(); } @@ -21,6 +86,8 @@ export class TreasuryController { */ @Get('risk-metrics') @Roles(UserRole.ADMIN, UserRole.TREASURER) + @ApiOperation({ summary: 'Calculate risk metrics for the treasury' }) + @ApiResponse({ status: 200, description: 'Risk metrics calculated' }) async calculateRiskMetrics() { return this.treasuryService.calculateRiskMetrics(); } @@ -30,6 +97,24 @@ export class TreasuryController { */ @Post('deposit') @Roles(UserRole.ADMIN, UserRole.TREASURER) + @ApiOperation({ summary: 'Process a deposit' }) + @ApiBody({ + schema: { + type: 'object', + properties: { + assetId: { type: 'string' }, + amount: { type: 'string' }, + fromAddress: { type: 'string', nullable: true }, + blockchainTxHash: { type: 'string', nullable: true }, + metadata: { + type: 'object', + additionalProperties: true, + nullable: true, + }, + }, + }, + }) + @ApiResponse({ status: 201, description: 'Deposit processed' }) async processDeposit( @Body() depositData: { @@ -54,6 +139,24 @@ export class TreasuryController { */ @Post('withdrawal') @Roles(UserRole.ADMIN, UserRole.TREASURER) + @ApiOperation({ summary: 'Process a withdrawal' }) + @ApiBody({ + schema: { + type: 'object', + properties: { + assetId: { type: 'string' }, + amount: { type: 'string' }, + toAddress: { type: 'string' }, + blockchainTxHash: { type: 'string', nullable: true }, + metadata: { + type: 'object', + additionalProperties: true, + nullable: true, + }, + }, + }, + }) + @ApiResponse({ status: 201, description: 'Withdrawal processed' }) async processWithdrawal( @Body() withdrawalData: { @@ -78,6 +181,16 @@ export class TreasuryController { */ @Post('budget/:id/approve') @Roles(UserRole.ADMIN) + @ApiOperation({ summary: 'Approve a budget' }) + @ApiBody({ + schema: { + type: 'object', + properties: { + approverId: { type: 'string' }, + }, + }, + }) + @ApiResponse({ status: 200, description: 'Budget approved' }) async approveBudget( @Param('id') budgetId: string, @Body() data: { approverId: string } @@ -93,6 +206,16 @@ export class TreasuryController { */ @Post('allocation/:id/approve') @Roles(UserRole.ADMIN, UserRole.TREASURER) + @ApiOperation({ summary: 'Approve an allocation' }) + @ApiBody({ + schema: { + type: 'object', + properties: { + approverId: { type: 'string' }, + }, + }, + }) + @ApiResponse({ status: 200, description: 'Allocation approved' }) async approveAllocation( @Param('id') allocationId: string, @Body() data: { approverId: string } @@ -108,6 +231,17 @@ export class TreasuryController { */ @Post('budget-with-allocation') @Roles(UserRole.ADMIN, UserRole.TREASURER) + @ApiOperation({ summary: 'Create a budget with allocation' }) + @ApiBody({ + schema: { + type: 'object', + properties: { + budget: { type: 'object', additionalProperties: true }, + allocation: { type: 'object', additionalProperties: true }, + }, + }, + }) + @ApiResponse({ status: 201, description: 'Budget with allocation created' }) async createBudgetWithAllocation( @Body() data: { budget: any; allocation: any } ) { @@ -122,6 +256,8 @@ export class TreasuryController { */ @Get('audit') @Roles(UserRole.ADMIN, UserRole.TREASURER, UserRole.AUDITOR) + @ApiOperation({ summary: 'Generate audit report' }) + @ApiResponse({ status: 200, description: 'Audit report generated' }) async generateAuditReport( @Query('fromDate') fromDate: string, @Query('toDate') toDate: string @@ -137,6 +273,8 @@ export class TreasuryController { */ @Post('housekeeping') @Roles(UserRole.ADMIN, UserRole.TREASURER) + @ApiOperation({ summary: 'Run treasury housekeeping tasks' }) + @ApiResponse({ status: 200, description: 'Housekeeping tasks completed' }) async performHousekeeping() { return this.treasuryService.performHousekeeping(); } diff --git a/src/modules/treasury/dto/create-treasury.dto.ts b/src/modules/treasury/dto/create-treasury.dto.ts new file mode 100644 index 0000000..ab84cfb --- /dev/null +++ b/src/modules/treasury/dto/create-treasury.dto.ts @@ -0,0 +1,15 @@ +import { IsString, IsNotEmpty, IsNumber } from 'class-validator'; + +export class CreateTreasuryDto { + @IsString() + @IsNotEmpty() + name: string; + + @IsString() + @IsNotEmpty() + organizationId: string; + + @IsNumber() + @IsNotEmpty() + initialBalance: number; +} diff --git a/src/modules/treasury/dto/update-treasury.dto.ts b/src/modules/treasury/dto/update-treasury.dto.ts new file mode 100644 index 0000000..325b1a2 --- /dev/null +++ b/src/modules/treasury/dto/update-treasury.dto.ts @@ -0,0 +1,12 @@ +import { IsString, IsOptional, IsNumber } from 'class-validator'; + +export class UpdateTreasuryDto { + @IsString() + @IsOptional() + name?: string; + + @IsNumber() + @IsOptional() + balance?: number; + initialBalance: undefined; +} diff --git a/src/modules/treasury/events/treasury.event-handler.ts b/src/modules/treasury/events/treasury.event-handler.ts new file mode 100644 index 0000000..45237b1 --- /dev/null +++ b/src/modules/treasury/events/treasury.event-handler.ts @@ -0,0 +1,28 @@ +import { OnEvent } from '@nestjs/event-emitter'; +import { Injectable } from '@nestjs/common'; +import { + TreasuryCreatedEvent, + TreasuryUpdatedEvent, + TreasuryDeletedEvent, +} from './treasury.events'; + +@Injectable() +export class TreasuryEventHandler { + @OnEvent('treasury.created') + handleCreatedEvent(payload: TreasuryCreatedEvent) { + console.log('Treasury Created:', payload.id, payload.name); + // Trigger sync, notification, analytics + } + + @OnEvent('treasury.updated') + handleUpdatedEvent(payload: TreasuryUpdatedEvent) { + console.log('Treasury Updated:', payload.id, payload.changes); + // Log changes or notify stakeholders + } + + @OnEvent('treasury.deleted') + handleDeletedEvent(payload: TreasuryDeletedEvent) { + console.log('Treasury Deleted:', payload.id); + // Clean up resources or log deletion + } +} diff --git a/src/modules/treasury/events/treasury.events.ts b/src/modules/treasury/events/treasury.events.ts new file mode 100644 index 0000000..42dc0bf --- /dev/null +++ b/src/modules/treasury/events/treasury.events.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; + +// Event payloads +export class TreasuryCreatedEvent { + constructor( + public readonly id: string, + public readonly name: string, + public readonly balance: number + ) {} +} + +export class TreasuryUpdatedEvent { + constructor( + public readonly id: string, + public readonly changes: Partial<{ + name: string; + balance: number; + }> + ) {} +} + +export class TreasuryDeletedEvent { + constructor(public readonly id: string) {} +} + +export class TreasuryFundsTransferredEvent { + constructor( + public readonly fromTreasuryId: string, + public readonly toTreasuryId: string, + public readonly amount: number + ) {} +} + +/** + * Helper service to emit events + */ +@Injectable() +export class TreasuryEventEmitter { + constructor(private readonly eventEmitter: EventEmitter2) {} + + emitCreated(event: TreasuryCreatedEvent): void { + this.eventEmitter.emit('treasury.created', event); + } + + emitUpdated(event: TreasuryUpdatedEvent): void { + this.eventEmitter.emit('treasury.updated', event); + } + + emitDeleted(event: TreasuryDeletedEvent): void { + this.eventEmitter.emit('treasury.deleted', event); + } +} diff --git a/src/modules/treasury/services/treasury.service.ts b/src/modules/treasury/services/treasury.service.ts index 7f0869e..c242e23 100644 --- a/src/modules/treasury/services/treasury.service.ts +++ b/src/modules/treasury/services/treasury.service.ts @@ -4,6 +4,8 @@ import { TreasuryAssetService } from './treasury-asset.service'; import { TreasuryTransactionService } from './treasury-transaction.service'; import { TreasuryBudgetService } from './treasury-budget.service'; import { TreasuryAllocationService } from './treasury-allocation.service'; +import { CreateTreasuryDto } from '../dto/create-treasury.dto'; +import { UpdateTreasuryDto } from '../dto/update-treasury.dto'; import { formatErrorMessage, BusinessLogicError, @@ -13,6 +15,15 @@ import { Asset } from '../entities/asset.entity'; import { TransactionType } from '../entities/asset-transaction.entity'; import { Budget, BudgetStatus } from '../entities/budget.entity'; import { Allocation, AllocationStatus } from '../entities/allocation.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Treasury } from 'src/modules/user/entities/treasury.entity'; +import { Repository } from 'typeorm'; +import { + TreasuryEventEmitter, + TreasuryCreatedEvent, + TreasuryUpdatedEvent, + TreasuryDeletedEvent, +} from '../events/treasury.events'; /** * Main Treasury Service that coordinates operations across the treasury module. @@ -25,6 +36,10 @@ export class TreasuryService { private transactionService: TreasuryTransactionService, private budgetService: TreasuryBudgetService, private allocationService: TreasuryAllocationService, + private treasuryEventEmitter: TreasuryEventEmitter, + @InjectRepository(Treasury) + private treasuryRepo: Repository, + @Inject(LoggingService) private logger: LoggingService ) { @@ -44,7 +59,52 @@ export class TreasuryService { } return proposal; } + async create(data: CreateTreasuryDto): Promise { + const treasury = this.treasuryRepo.create({ + name: data.name, + totalBalance: data.initialBalance || 0, + }); + const saved = await this.treasuryRepo.save(treasury); + + this.treasuryEventEmitter.emitCreated( + new TreasuryCreatedEvent(saved.id, saved.name, saved.totalBalance) + ); + + return saved; + } + + async findTreasuryById(id: string): Promise { + const treasury = await this.treasuryRepo.findOneBy({ id }); + if (!treasury) + throw new NotFoundException(`Treasury with ID ${id} not found`); + return treasury; + } + async update(id: string, data: UpdateTreasuryDto): Promise { + const treasury = await this.findTreasuryById(id); + // const previousBalance = treasury.totalBalance; + + Object.assign(treasury, data); + const updated = await this.treasuryRepo.save(treasury); + + const changes: Partial<{ name: string; balance: number }> = {}; + if (data.name) changes.name = data.name; + if (data.initialBalance !== undefined) + changes.balance = data.initialBalance; + + this.treasuryEventEmitter.emitUpdated( + new TreasuryUpdatedEvent(id, changes) + ); + + return updated; + } + + async delete(id: string): Promise { + const treasury = await this.findTreasuryById(id); + await this.treasuryRepo.remove(treasury); + + this.treasuryEventEmitter.emitDeleted(new TreasuryDeletedEvent(id)); + } /** * Get treasury overview with balances, allocations, and recent activity */ diff --git a/src/modules/treasury/treasury.module.ts b/src/modules/treasury/treasury.module.ts index bdadc5c..25c0e62 100644 --- a/src/modules/treasury/treasury.module.ts +++ b/src/modules/treasury/treasury.module.ts @@ -18,9 +18,13 @@ import { Budget } from './entities/budget.entity'; import { Allocation } from './entities/allocation.entity'; import { AllocationTransaction } from './entities/allocation-transaction.entity'; import { Asset } from './entities/asset.entity'; +import { EventEmitterModule } from '@nestjs/event-emitter'; +import { TreasuryEventHandler } from './events/treasury.event-handler'; +import { TreasuryEventEmitter } from './events/treasury.events'; @Module({ imports: [ + EventEmitterModule.forRoot(), TypeOrmModule.forFeature([ Transaction, LedgerEntry, @@ -42,6 +46,8 @@ import { Asset } from './entities/asset.entity'; TreasuryAssetService, LoggingService, StarknetService, + TreasuryEventEmitter, + TreasuryEventHandler, ], controllers: [TransactionController], exports: [TreasuryService], diff --git a/src/modules/user/entities/user.entity.ts b/src/modules/user/entities/user.entity.ts index 6763b0a..846cbd1 100644 --- a/src/modules/user/entities/user.entity.ts +++ b/src/modules/user/entities/user.entity.ts @@ -4,9 +4,8 @@ import { PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, - BeforeInsert, + } from 'typeorm'; -import * as bcrypt from 'bcrypt'; export enum UserRole { USER = 'user', @@ -58,11 +57,5 @@ export class User { @UpdateDateColumn() updatedAt: Date; - /** 🔐 Hash password before inserting into the database */ - @BeforeInsert() - async hashPassword() { - if (this.password) { - this.password = await bcrypt.hash(this.password, 10); - } - } + }