From 6048ae55002928d9984718952cf7bfb920289eac Mon Sep 17 00:00:00 2001 From: Thiago Felipe Date: Mon, 27 Oct 2025 19:09:19 -0300 Subject: [PATCH 01/13] feat(wallet): add initial prisma schema for Wallet and WalletItem --- prisma/schema.prisma | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) 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]) +} From 905d3eba492ef21d7d96e00eca004179609c0d14 Mon Sep 17 00:00:00 2001 From: Thiago Felipe Date: Mon, 27 Oct 2025 19:10:33 -0300 Subject: [PATCH 02/13] feat(wallet): create Wallet controller --- src/wallet/wallet.controller.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/wallet/wallet.controller.ts 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); + } +} From bdc5e87d7edb0bede434db755f29cff29aa654c9 Mon Sep 17 00:00:00 2001 From: Thiago Felipe Date: Mon, 27 Oct 2025 19:10:57 -0300 Subject: [PATCH 03/13] feat(wallet): create Wallet module --- src/wallet/wallet.module.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/wallet/wallet.module.ts 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 {} From 0ce120c98de3c34ddf974f8c9d878b16694b8988 Mon Sep 17 00:00:00 2001 From: Thiago Felipe Date: Mon, 27 Oct 2025 19:11:31 -0300 Subject: [PATCH 04/13] feat(wallet): create Wallet service --- src/wallet/wallet.service.ts | 66 ++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/wallet/wallet.service.ts 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, + }; + } +} From 2efd283ef5a268a63b6f6b824e85330dd9d7ed9a Mon Sep 17 00:00:00 2001 From: Thiago Felipe Date: Mon, 27 Oct 2025 19:13:11 -0300 Subject: [PATCH 05/13] test(wallet): add basic unit tests for WalletService --- src/wallet/tests/wallet.service.spec.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/wallet/tests/wallet.service.spec.ts 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'); + }); +}); From 48cb30ef79979d781f98c9e72116493ec973fd30 Mon Sep 17 00:00:00 2001 From: Thiago Felipe Date: Mon, 27 Oct 2025 19:15:19 -0300 Subject: [PATCH 06/13] test(wallet): add e2e jest+supertest tests --- src/wallet/tests/wallet.e2e-spec.ts | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/wallet/tests/wallet.e2e-spec.ts diff --git a/src/wallet/tests/wallet.e2e-spec.ts b/src/wallet/tests/wallet.e2e-spec.ts new file mode 100644 index 0000000..890e294 --- /dev/null +++ b/src/wallet/tests/wallet.e2e-spec.ts @@ -0,0 +1,31 @@ +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'); + expect(res.status).toBe(200); + expect(res.body).toBeDefined(); + }); + + afterAll(async () => { + await app.close(); + }); +}); From 331d890a7ebd6b6b8e602dfdda231aaf41d37849 Mon Sep 17 00:00:00 2001 From: Thiago Felipe Date: Mon, 27 Oct 2025 19:20:08 -0300 Subject: [PATCH 07/13] fix(test): in line three, removed " * as " causing an error while running e2e test --- test/app.e2e-spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index 4df6580..36852c5 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; +import request from 'supertest'; import { App } from 'supertest/types'; import { AppModule } from './../src/app.module'; From 5f3f39f376151a4a253e90bcca535aa1abe6e1fd Mon Sep 17 00:00:00 2001 From: Thiago Felipe Date: Mon, 27 Oct 2025 19:22:05 -0300 Subject: [PATCH 08/13] feat(db): table creation of Wallet and WalletItem --- .../migration.sql | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 prisma/migrations/20251026160546_create_wallet_models/migration.sql 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; From f9115e71005f0069ab061a5bd6ffcd1f6bff7678 Mon Sep 17 00:00:00 2001 From: Thiago Felipe Date: Wed, 5 Nov 2025 09:58:36 -0300 Subject: [PATCH 09/13] chore: adding WalletModule to be referenced. --- src/app.module.ts | 2 ++ 1 file changed, 2 insertions(+) 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, }), From 9298f2e9bdf7826a657fc744dc8101a05ab426e8 Mon Sep 17 00:00:00 2001 From: Thiago Felipe Date: Wed, 5 Nov 2025 10:05:48 -0300 Subject: [PATCH 10/13] chore: test removed from wallet folder. --- src/wallet/tests/wallet.e2e-spec.ts | 31 ----------------------------- 1 file changed, 31 deletions(-) delete mode 100644 src/wallet/tests/wallet.e2e-spec.ts diff --git a/src/wallet/tests/wallet.e2e-spec.ts b/src/wallet/tests/wallet.e2e-spec.ts deleted file mode 100644 index 890e294..0000000 --- a/src/wallet/tests/wallet.e2e-spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -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'); - expect(res.status).toBe(200); - expect(res.body).toBeDefined(); - }); - - afterAll(async () => { - await app.close(); - }); -}); From e04e0b119e4c4b2350a6f44fd2e4a1596bc621d2 Mon Sep 17 00:00:00 2001 From: Thiago Felipe Date: Wed, 5 Nov 2025 10:07:46 -0300 Subject: [PATCH 11/13] refactor: removed general test. --- test/app.e2e-spec.ts | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 test/app.e2e-spec.ts diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts deleted file mode 100644 index 36852c5..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 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!'); - }); -}); From cf307a41497b93c645830dd9d1654348359c0ebe Mon Sep 17 00:00:00 2001 From: Thiago Felipe Date: Wed, 5 Nov 2025 10:08:16 -0300 Subject: [PATCH 12/13] refactor: removed general test. --- test/db.e2e-spec.ts | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 test/db.e2e-spec.ts 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"); - }); -}); From 12d5bd9adaca23cc84afb9e05a1f05e6bb404ea2 Mon Sep 17 00:00:00 2001 From: Thiago Felipe Date: Wed, 5 Nov 2025 10:10:34 -0300 Subject: [PATCH 13/13] chore: adding wallet test in test folder. --- test/wallet.e2e-spec.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/wallet.e2e-spec.ts 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(); + }); +});