Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/entities/wallet/wallet.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
type WalletProps = {
id?: string;
userId: string;
name: string;
initialBalance: number;
isDefault: boolean;

createdAt: Date;
updatedAt: Date;
deletedAt?: Date;
};

export class Wallet {
constructor(private readonly props: WalletProps) {}

get id(): string | undefined {
return this.props.id;
}

set id(id: string | undefined) {
this.props.id = id;
}

get userId(): string {
return this.props.userId;
}

set userId(userId: string) {
this.props.userId = userId;
}

get name(): string {
return this.props.name;
}

set name(name: string) {
this.props.name = name;
}

get initialBalance(): number {
return this.props.initialBalance;
}

set initialBalance(balance: number) {
this.props.initialBalance = balance;
}

get isDefault(): boolean {
return this.props.isDefault;
}

set isDefault(isDefault: boolean) {
this.props.isDefault = isDefault;
}

get createdAt(): Date {
return this.props.createdAt;
}

set createdAt(createdAt: Date) {
this.props.createdAt = createdAt;
}

get updatedAt(): Date {
return this.props.updatedAt;
}

set updatedAt(updatedAt: Date) {
this.props.updatedAt = updatedAt;
}

get deletedAt(): Date | undefined {
return this.props.deletedAt;
}

set deletedAt(deletedAt: Date | undefined) {
this.props.deletedAt = deletedAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE wallets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
name VARCHAR(100) NOT NULL,
initial_balance DECIMAL(15, 2) NOT NULL DEFAULT 0.00,
is_default BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
`);

pgm.sql(`
CREATE INDEX idx_wallets_user_id
ON wallets (user_id);
`);

pgm.sql(`
CREATE UNIQUE INDEX idx_wallets_default_per_user
ON wallets (user_id)
WHERE is_default = true AND deleted_at IS NULL;
`);
Comment thread
daniloamsilva marked this conversation as resolved.
};

exports.down = (pgm) => {
pgm.sql('DROP TABLE wallets');
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,34 @@ import { HttpStatus } from '@nestjs/common';
import { Queue } from 'bullmq';

import { UsersRepositoryInterface } from '@/repositories/users/users.repository.interface';
import { WalletsRepositoryInterface } from '@/repositories/wallets/wallets.repository.interface';
import { CreateUserService } from './create-user.service';
import { UsersInMemoryRepository } from '@/repositories/users/users-in-memory.repository';
import { WalletsInMemoryRepository } from '@/repositories/wallets/wallets-in-memory.repository';
import { UserFactory } from '@/entities/user/user.factory';
import { QueuesService } from '@/infra/queues/queues.service';

describe('CreateUserService', () => {
let createUserService: CreateUserService;
let queuesService: QueuesService;
let usersRepository: UsersRepositoryInterface;
let walletsRepository: WalletsRepositoryInterface;
let confirmationEmailQueue: Queue;

beforeEach(() => {
usersRepository = new UsersInMemoryRepository();
walletsRepository = new WalletsInMemoryRepository();

confirmationEmailQueue = {
add: jest.fn(),
} as unknown as Queue;

queuesService = new QueuesService(confirmationEmailQueue, {} as Queue);
createUserService = new CreateUserService(usersRepository, queuesService);
createUserService = new CreateUserService(
usersRepository,
walletsRepository,
queuesService,
);
});

it('should not be able to create a new user with the an email already used', async () => {
Expand All @@ -46,6 +54,7 @@ describe('CreateUserService', () => {
});

const newUser = await usersRepository.findByEmail('johndoe@email.com');
const newWallets = await walletsRepository.findByUserId(newUser.id);

expect(newUser.id).toBeDefined();
expect(newUser.name).toBe('John Doe');
Expand All @@ -55,6 +64,15 @@ describe('CreateUserService', () => {
expect(newUser.updatedAt).not.toBeNull();
expect(newUser.deletedAt).toBeNull();

expect(newWallets.length).toBe(1);
expect(newWallets[0].userId).toBe(newUser.id);
expect(newWallets[0].name).toBe('Carteira');
expect(newWallets[0].initialBalance).toBe(0);
expect(newWallets[0].isDefault).toBe(true);
expect(newWallets[0].createdAt).not.toBeNull();
expect(newWallets[0].updatedAt).not.toBeNull();
expect(newWallets[0].deletedAt).toBeNull();

expect(confirmationEmailQueue.add).toHaveBeenCalledTimes(1);

expect(result).toMatchObject({
Expand Down
15 changes: 15 additions & 0 deletions src/models/users/use-cases/create-user/create-user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
import { CreateUserRequestDto } from './create-user.request.dto';
import { CreateUserResponseDto } from './create-user.response.dto';
import { User } from '@/entities/user/user.entity';
import { Wallet } from '@/entities/wallet/wallet.entity';
import { UsersRepositoryInterface } from '@/repositories/users/users.repository.interface';
import { WalletsRepositoryInterface } from '@/repositories/wallets/wallets.repository.interface';
import { Providers } from '@/repositories/providers.enum';
import { QueuesService } from '@/infra/queues/queues.service';
import { QueueType } from '@/infra/queues/queues.enum';
Expand All @@ -18,6 +20,8 @@ export class CreateUserService {
constructor(
@Inject(Providers.USERS_REPOSITORY)
private readonly usersRepository: UsersRepositoryInterface,
@Inject(Providers.WALLETS_REPOSITORY)
private readonly walletsRepository: WalletsRepositoryInterface,
private readonly queuesService: QueuesService,
) {}

Expand All @@ -38,6 +42,17 @@ export class CreateUserService {
});
await this.usersRepository.save(user);

const wallet = new Wallet({
userId: user.id,
name: 'Carteira',
initialBalance: 0,
isDefault: true,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
});
await this.walletsRepository.save(wallet);

await this.queuesService.execute({
userId: user.id,
queueType: QueueType.CONFIRMATION_EMAIL,
Expand Down
5 changes: 5 additions & 0 deletions src/models/users/users.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { ResetPasswordService } from './use-cases/reset-password/reset-password.

import { UsersPostgresRepository } from '@/repositories/users/users-postgres.repository';
import { TokensPostgresRepository } from '@/repositories/tokens/tokens-postgres.repository';
import { WalletsPostgresRepository } from '@/repositories/wallets/wallets-postgres.repository';

@Module({
controllers: [
Expand Down Expand Up @@ -51,6 +52,10 @@ import { TokensPostgresRepository } from '@/repositories/tokens/tokens-postgres.
provide: Providers.TOKENS_REPOSITORY,
useClass: TokensPostgresRepository,
},
{
provide: Providers.WALLETS_REPOSITORY,
useClass: WalletsPostgresRepository,
},
],
})
export class UsersModule {}
1 change: 1 addition & 0 deletions src/repositories/providers.enum.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum Providers {
USERS_REPOSITORY = 'UsersRepository',
TOKENS_REPOSITORY = 'TokensRepository',
WALLETS_REPOSITORY = 'WalletsRepository',
}
26 changes: 26 additions & 0 deletions src/repositories/wallets/wallets-in-memory.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { v7 as uuid } from 'uuid';

import { WalletsRepositoryInterface } from './wallets.repository.interface';
import { Wallet } from '@/entities/wallet/wallet.entity';

export class WalletsInMemoryRepository implements WalletsRepositoryInterface {
private wallets: Wallet[] = [];

async save(wallet: Wallet): Promise<Wallet> {
if (wallet.id) {
const index = this.wallets.findIndex((w) => w.id === wallet.id);
this.wallets[index] = wallet;
} else {
wallet.id = uuid();
this.wallets.push(wallet);
}

return wallet;
}
Comment thread
daniloamsilva marked this conversation as resolved.

async findByUserId(userId: string): Promise<Wallet[]> {
return this.wallets.filter(
(wallet) => wallet.userId === userId && !wallet.deletedAt,
);
}
Comment thread
daniloamsilva marked this conversation as resolved.
}
76 changes: 76 additions & 0 deletions src/repositories/wallets/wallets-postgres.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Inject } from '@nestjs/common';

import { WalletsRepositoryInterface } from './wallets.repository.interface';
import { DatabaseService } from '@/infra/database/database.service';
import { Wallet } from '@/entities/wallet/wallet.entity';
import { mapSnakeToCamel } from '@/utils/map-snake-to-camel';

export class WalletsPostgresRepository implements WalletsRepositoryInterface {
constructor(
@Inject(DatabaseService)
private readonly database: DatabaseService,
) {}

async save(wallet: Wallet): Promise<Wallet> {
const findWallet = (await this.findByUserId(wallet.userId)).find(
(w) => w.id === wallet.id,
);

let query = '';
let values = [];

if (findWallet) {
query = `
UPDATE wallets
SET
user_id = $1,
name = $2,
initial_balance = $3,
is_default = $4,
updated_at = $5
WHERE id = $6
RETURNING *;
`;

values = [
wallet.userId,
wallet.name,
wallet.initialBalance,
wallet.isDefault,
wallet.updatedAt,
findWallet.id,
];
} else {
query = `
INSERT INTO wallets (user_id, name, initial_balance, is_default, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *;
`;

values = [
wallet.userId,
wallet.name,
wallet.initialBalance,
wallet.isDefault,
wallet.createdAt,
wallet.updatedAt,
];
}

const rows = await this.database.query(query, values);
return new Wallet(mapSnakeToCamel(rows[0]));
}
Comment thread
daniloamsilva marked this conversation as resolved.

async findByUserId(userId: string): Promise<Wallet[]> {
const rows = await this.database.query(
`
SELECT * FROM wallets
WHERE user_id = $1
AND deleted_at IS NULL;
`,
[userId],
);

return rows.map((row) => new Wallet(mapSnakeToCamel(row)));
}
Comment thread
daniloamsilva marked this conversation as resolved.
}
6 changes: 6 additions & 0 deletions src/repositories/wallets/wallets.repository.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Wallet } from '@/entities/wallet/wallet.entity';

export interface WalletsRepositoryInterface {
save(wallet: Wallet): Promise<Wallet>;
findByUserId(userId: string): Promise<Wallet[]>;
}
Loading