diff --git a/prisma/migrations/20251026160546_create_wallet_models/migration.sql b/prisma/migrations/20251026160546_create_wallet_models/migration.sql new file mode 100644 index 0000000..eb2e8be --- /dev/null +++ b/prisma/migrations/20251026160546_create_wallet_models/migration.sql @@ -0,0 +1,34 @@ +-- CreateTable +CREATE TABLE "public"."Wallet" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "totalValue" DOUBLE PRECISION NOT NULL DEFAULT 0, + "totalProfit" DOUBLE PRECISION NOT NULL DEFAULT 0, + "rentability" DOUBLE PRECISION NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Wallet_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."WalletItem" ( + "id" TEXT NOT NULL, + "walletId" TEXT NOT NULL, + "assetId" TEXT NOT NULL, + "quantity" DOUBLE PRECISION NOT NULL, + "avgBuyPrice" DOUBLE PRECISION NOT NULL, + "currentPrice" DOUBLE PRECISION NOT NULL, + "profit" DOUBLE PRECISION NOT NULL, + + CONSTRAINT "WalletItem_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Wallet_userId_key" ON "public"."Wallet"("userId"); + +-- AddForeignKey +ALTER TABLE "public"."Wallet" ADD CONSTRAINT "Wallet_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."WalletItem" ADD CONSTRAINT "WalletItem_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "public"."Wallet"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3f3c8ee..aeaf234 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,4 +21,29 @@ model User { role String @default("user") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt -} \ No newline at end of file + wallet Wallet? +} + +model Wallet { + id String @id @default(uuid()) + userId String @unique + totalValue Float @default(0) + totalProfit Float @default(0) + rentability Float @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + items WalletItem[] + user User @relation(fields: [userId], references: [id]) +} + +model WalletItem { + id String @id @default(uuid()) + walletId String + assetId String + quantity Float + avgBuyPrice Float + currentPrice Float + profit Float + wallet Wallet @relation(fields: [walletId], references: [id]) + // assets Assets @relation(fields: [assetId], references: [id]) +} diff --git a/src/app.module.ts b/src/app.module.ts index 96db2a6..b30a7d7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,12 +5,14 @@ import { AssetsModule } from "./assets/assets.module"; import { UsersModule } from "./users/users.module"; import { AuthModule } from "./auth/auth.module"; import { ConfigModule } from "@nestjs/config"; +import { WalletModule } from "./wallet/wallet.module"; @Module({ imports: [ AssetsModule, UsersModule, AuthModule, + WalletModule, ConfigModule.forRoot({ isGlobal: true, }), diff --git a/src/wallet/tests/wallet.service.spec.ts b/src/wallet/tests/wallet.service.spec.ts new file mode 100644 index 0000000..8fd4915 --- /dev/null +++ b/src/wallet/tests/wallet.service.spec.ts @@ -0,0 +1,25 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { WalletService } from '../wallet.service'; +import { PrismaService } from '../../common/prisma.service'; + +describe('WalletService', () => { + let service: WalletService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [WalletService, PrismaService], + }).compile(); + + service = module.get(WalletService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should return wallet summary', async () => { + const summary = await service.getWalletSummary('test-user'); + expect(summary).toHaveProperty('totalValue'); + expect(summary).toHaveProperty('totalProfit'); + }); +}); diff --git a/src/wallet/wallet.controller.ts b/src/wallet/wallet.controller.ts new file mode 100644 index 0000000..7c04bab --- /dev/null +++ b/src/wallet/wallet.controller.ts @@ -0,0 +1,17 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { WalletService } from './wallet.service'; + +@Controller('wallet') +export class WalletController { + constructor(private readonly walletService: WalletService) {} + + @Get() + async getWallet(@Query('userId') userId: string) { + return this.walletService.getWallet(userId); + } + + @Get('summary') + async getWalletSummary(@Query('userId') userId: string) { + return this.walletService.getWalletSummary(userId); + } +} diff --git a/src/wallet/wallet.module.ts b/src/wallet/wallet.module.ts new file mode 100644 index 0000000..d336ded --- /dev/null +++ b/src/wallet/wallet.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { WalletService } from './wallet.service'; +import { WalletController } from './wallet.controller'; +import { PrismaService } from '../common/prisma.service'; + +@Module({ + providers: [WalletService, PrismaService], + controllers: [WalletController], +}) +export class WalletModule {} diff --git a/src/wallet/wallet.service.ts b/src/wallet/wallet.service.ts new file mode 100644 index 0000000..e2ae2bb --- /dev/null +++ b/src/wallet/wallet.service.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../common/prisma.service'; + +@Injectable() +export class WalletService { + constructor(private prisma: PrismaService) {} + + // Retorna ativos detalhados na carteira do usuário + async getWallet(userId: string) { + // const orders = await this.prisma.order.findMany({ + // where: { userId }, + // include: { asset: true }, + // }); + + const orders = [ + { assetId: '1', quantity: 10, price: 50, type: 'BUY', asset: { symbol: 'TEC11', name: 'Tesouro', currentPrice: 55 } }, + { assetId: '2', quantity: 5, price: 100, type: 'BUY', asset: { symbol: 'FINV3', name: 'Financeira', currentPrice: 95 } }, +]; + + const grouped = new Map(); + + for (const order of orders) { + const { assetId, asset, quantity, price, type } = order; + const sign = type === 'BUY' ? 1 : -1; + const entry = grouped.get(assetId) || { + assetId, + symbol: asset.symbol, + name: asset.name, + quantity: 0, + totalSpent: 0, + avgBuyPrice: 0, + currentPrice: asset.currentPrice, + }; + + entry.quantity += quantity * sign; + entry.totalSpent += price * quantity * (type === 'BUY' ? 1 : -1); + entry.avgBuyPrice = entry.totalSpent / entry.quantity; + grouped.set(assetId, entry); + } + + const walletItems = Array.from(grouped.values()).map((item) => { + const profit = (item.currentPrice - item.avgBuyPrice) * item.quantity; + return { ...item, profit }; + }); + + return walletItems; + } + + // Resumo da carteira + async getWalletSummary(userId: string) { + const wallet = await this.getWallet(userId); + const totalValue = wallet.reduce( + (acc, a) => acc + a.currentPrice * a.quantity, + 0, + ); + const totalProfit = wallet.reduce((acc, a) => acc + a.profit, 0); + const rentability = totalProfit / (totalValue - totalProfit || 1); + + return { + totalValue, + totalProfit, + rentability, + assetsCount: wallet.length, + }; + } +} diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts deleted file mode 100644 index 4df6580..0000000 --- a/test/app.e2e-spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; -import { App } from 'supertest/types'; -import { AppModule } from './../src/app.module'; - -describe('AppController (e2e)', () => { - let app: INestApplication; - - beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); - }); - - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); - }); -}); diff --git a/test/db.e2e-spec.ts b/test/db.e2e-spec.ts deleted file mode 100644 index c36b21f..0000000 --- a/test/db.e2e-spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Client } from "pg"; - -describe("Database basic test", () => { - let client: Client; - - beforeAll(async () => { - client = new Client({ - host: process.env.DB_HOST || "localhost", - port: +(process.env.DB_PORT || 5433), - user: process.env.DB_USER || "user", - password: process.env.DB_PASS || "bolsa2024!", - database: process.env.DB_NAME || "bolsa_sim", - }); - await client.connect(); - }); - - afterAll(async () => { - await client.end(); - }); - - it("should create table test and insert a row", async () => { - await client.query(`CREATE TABLE IF NOT EXISTS test ( - id SERIAL PRIMARY KEY, - name TEXT - )`); - const insertResult = await client.query( - "INSERT INTO test (name) VALUES ($1) RETURNING id, name", - ["registro de teste"] - ); - expect(insertResult.rows[0]).toHaveProperty("id"); - expect(insertResult.rows[0].name).toBe("registro de teste"); - }); -}); diff --git a/test/wallet.e2e-spec.ts b/test/wallet.e2e-spec.ts new file mode 100644 index 0000000..43f384a --- /dev/null +++ b/test/wallet.e2e-spec.ts @@ -0,0 +1,33 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; +import { AppModule } from '../src/app.module'; + +describe('Wallet (e2e)', () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }) + //aqui desabilitamos o JwtStrategy só para o teste + .overrideProvider('JwtStrategy') + .useValue({}) + .compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it("/wallet (GET)", async () => { + const res = await request(app.getHttpServer()) + .get("/wallet") + .query({ userId: "admin-001" }); + expect(res.status).toBe(200); + expect(res.body).toBeDefined(); + }); + + afterAll(async () => { + await app.close(); + }); +});