diff --git a/.vscode/settings.json b/.vscode/settings.json index b6fc9e8..9623b34 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { + "files.eol": "\r\n", "editor.defaultFormatter": "biomejs.biome", "editor.formatOnSave": true, "files.autoSave": "off", @@ -8,5 +9,6 @@ "[prisma]": { "editor.defaultFormatter": "Prisma.prisma", "editor.formatOnSave": true - } + }, + "cSpell.words": ["bitnami", "zipcode"] } diff --git a/README.md b/README.md index b455145..8254000 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,275 @@ -# TCC - Pizzaria +# 🍕 Pizza Stars - API Backend -Esse é o back-end de um projeto de tcc, na qual estamos desenvolvendo para uma pizzaria. +> Sistema de gerenciamento para pizzaria desenvolvido como Trabalho de Conclusão de Curso (TCC) + +Este é o backend de um sistema completo para pizzaria, oferecendo funcionalidades de autenticação, gerenciamento de usuários, produtos e pedidos. + +## 📋 Sumário + +- [Funcionalidades](#-funcionalidades) +- [Tecnologias](#-tecnologias) +- [Pré-requisitos](#-pré-requisitos) +- [Instalação](#-instalação) +- [Configuração](#-configuração) +- [Uso](#-uso) +- [API Endpoints](#-api-endpoints) +- [Banco de Dados](#-banco-de-dados) +- [Scripts Disponíveis](#-scripts-disponíveis) +- [Estrutura do Projeto](#-estrutura-do-projeto) +- [Contribuição](#-contribuição) + +## 🚀 Funcionalidades + +### Autenticação e Autorização +- ✅ Registro de usuários +- ✅ Login com JWT +- ✅ Refresh token +- ✅ Logout +- ✅ Controle de acesso por roles (CUSTOMER, EMPLOYEE, ADMIN) + +### Gerenciamento de Usuários +- ✅ Perfil do usuário +- ✅ Atualização de dados +- ✅ Remoção de conta +- ✅ Listagem de usuários (Admin) + +### Sistema de Produtos +- 🍕 **Pizzas**: Diferentes tamanhos (Média, Grande, Família) e tipos (Doce, Salgada) +- 🥤 **Bebidas**: Categorias (Refrigerante, Suco, Alcoólica) com volume +- 🧁 **Sobremesas**: Tipos variados (Doce, Salgada) + +### Sistema de Pedidos +- 🛒 Carrinho de compras +- 📦 Gerenciamento de pedidos +- 📍 Sistema de endereços +- 🔄 Status de pedidos (Pendente, Processando, Entregando, Entregue, Cancelado) + +## 🛠 Tecnologias + +- **Runtime**: Node.js 20 +- **Framework**: Fastify +- **Database**: PostgreSQL +- **ORM**: Prisma +- **Autenticação**: JWT (@fastify/jwt) +- **Validação**: Zod +- **Criptografia**: bcryptjs +- **Linguagem principal**: TypeScript +- **Build**: tsup +- **Dev Tools**: tsx, biome (linting) + +## 📋 Pré-requisitos + +- Node.js 20 ou superior +- Docker e Docker Compose +- PostgreSQL (via Docker) + +## 🔧 Instalação + +1. **Clone o repositório** + ```bash + git clone https://github.com/MLKP1/Back.git + cd Back + ``` + +2. **Instale as dependências** + ```bash + npm install + ``` + +3. **Configuração do banco de dados** + ```bash + # Suba o container PostgreSQL + docker-compose up -d + ``` + +## ⚙️ Configuração + +### Variáveis de Ambiente + +Crie um arquivo `.env` na raiz do projeto: + +```env +# Database +DATABASE_URL="postgresql://lucas:lucas123@localhost:5432/pizza-stars" + +# Server +PORT=3333 +NODE_ENV="dev" + +# JWT +JWT_SECRET="sua-chave-secreta-super-segura" + +# Token de exemplo (opcional) +TOKEN="seu-token-de-teste" +``` + +### Configuração do Banco + +```bash +# Execute as migrações +npx prisma migrate dev + +# Opcional: Popular com dados de exemplo +npx prisma db seed +``` + +## 🚀 Uso + +### Desenvolvimento +```bash +npm run dev +``` +O servidor será iniciado em `http://localhost:3333` + +### Produção +```bash +# Build +npm run build + +# Start +npm start +``` + +### Outros comandos úteis +```bash +# Linting +npm run lint + +# Commit padronizado +npm run commit + +# Visualizar banco de dados +npx prisma studio +``` + +## 📚 API Endpoints + +### Autenticação +| Método | Endpoint | Descrição | Auth | +|--------|----------|-----------|------| +| `POST` | `/auth/login` | Login | ❌ | +| `PATCH` | `/auth/refresh` | Renovar token | ✅ | +| `DELETE` | `/auth/logout` | Logout | ✅ | + +### Usuários +| Método | Endpoint | Descrição | Auth | +|--------|----------|-----------|------| +| `POST` | `/users` | Registrar usuário | ❌ | +| `GET` | `/user` | Perfil do usuário | ✅ | +| `PATCH` | `/user` | Atualizar perfil | ✅ | +| `DELETE` | `/user` | Remover conta | ✅ | +| `GET` | `/users` | Listar usuários | 👑 Admin | + +### Headers de Autenticação +```http +Authorization: Bearer +``` + +### Exemplo de Requisição + +```javascript +// Registro de usuário +POST /users +Content-Type: application/json + +{ + "name": "Lucas", + "email": "lucas@gmail.com", + "password": "senha123" +} +``` + +## 💾 Banco de Dados + +### Modelos Principais + +- **User**: Usuários do sistema com roles +- **Address**: Endereços dos usuários +- **Pizza**: Produtos pizza com tamanhos e tipos +- **Drink**: Bebidas com categorias e volumes +- **Dessert**: Sobremesas variadas +- **Order**: Pedidos dos clientes +- **Cart**: Carrinho de compras + +### Diagrama ER + +#### Entidades e Relacionamentos Principais + +**Usuário (User)** +- → Tem um Endereço (Address) [1:1] +- → Tem um Carrinho (Cart) [1:1] +- → Realiza vários Pedidos (Order) [1:N] + +**Pedido (Order)** +- ← Pertence a um Usuário (User) [N:1] +- → Contém várias Pizzas (Pizza) [N:N] +- → Contém várias Bebidas (Drink) [N:N] +- → Contém várias Sobremesas (Dessert) [N:N] + +**Carrinho (Cart)** +- ← Pertence a um Usuário (User) [1:1] +- → Contém várias Pizzas (Pizza) [N:N] +- → Contém várias Bebidas (Drink) [N:N] +- → Contém várias Sobremesas (Dessert) [N:N] + +## 📜 Scripts Disponíveis + +| Script | Descrição | +|--------|-----------| +| `npm run dev` | Inicia servidor de desenvolvimento | +| `npm run build` | Build para produção | +| `npm start` | Inicia servidor de produção | +| `npm run lint` | Executa linting | +| `npm run commit` | Commit padronizado | + +## 📁 Estrutura do Projeto + +``` +prisma/ +├── schema.prisma # Schema do banco +├── seed.ts # Dados iniciais +└── migrations/ # Histórico de migrações + +src/ +└── @types/ # Definições de tipos TypeScript +├── env/ # Configuração de variáveis +├── http/ +│ ├── controllers/ # Controladores das rotas +│ │ └── users/ # Rotas de usuários +│ └── middlewares/ # Middlewares de autenticação +├── lib/ # Bibliotecas e configurações +├── repositories/ # Camada de dados +├── services/ # Lógica de negócio +│ ├── errors/ # Classes de erro customizadas +│ ├── factories/ # Factory pattern para services +│ └── users/ # Serviços de usuário +├── app.ts # Configuração do Fastify +├── server.ts # Servidor principal +``` + +## 🎯 Roadmap + +- [ ] API de produtos (CRUD pizzas, bebidas, sobremesas) +- [ ] Sistema completo de pedidos +- [ ] Integração com pagamentos +- [ ] Sistema de delivery +- [ ] Dashboard administrativo +- [ ] Notificações em tempo real +- [ ] Relatórios e analytics + +## 🤝 Contribuição + +1. Fork o projeto +2. Crie uma branch para sua feature (`git checkout -b feat/funcionalidade-legal`) +3. Faça linting e formatação (`npx biome check --write`) +4. Commit suas mudanças (`git commit -m 'feat: Adicionar uma funcionalidade legal'`) +5. Push para a branch (`git push origin feat/funcionalidade-legal`) +6. Abra um Pull Request + +## 📝 Licença + +Este projeto é desenvolvido como Trabalho de Conclusão de Curso (TCC). + +--- + +⭐ **Pizza Stars** - Transformando a experiência da sua pizzaria favorita! diff --git a/prisma/migrations/20250724210200_mudar_dois_campos_da_tabela_address/migration.sql b/prisma/migrations/20250724210200_mudar_dois_campos_da_tabela_address/migration.sql new file mode 100644 index 0000000..a57fa57 --- /dev/null +++ b/prisma/migrations/20250724210200_mudar_dois_campos_da_tabela_address/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - Changed the type of `zip_code` on the `addresses` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + +*/ +-- AlterTable +ALTER TABLE "addresses" ALTER COLUMN "complement" DROP NOT NULL, +DROP COLUMN "zip_code", +ADD COLUMN "zip_code" INTEGER NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7b3b1e6..6849d5f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,15 +8,15 @@ datasource db { } model Address { - id String @id @default(cuid()) + id String @id @default(cuid()) number Int street String neighborhood String - complement String + complement String? city String - state String @default("SP") - country String @default("BR") - zipCode String @map("zip_code") + state String @default("SP") + country String @default("BR") + zipCode Int @map("zip_code") latitude Float longitude Float diff --git a/prisma/seed.ts b/prisma/seed.ts index 33f110b..6e8de3d 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -57,7 +57,7 @@ async function seed() { street: faker.location.street(), neighborhood: faker.location.secondaryAddress(), number: Number.parseInt(faker.location.buildingNumber()), - zipCode: faker.location.zipCode(), + zipCode: Number.parseInt(faker.location.zipCode()), latitude: faker.location.latitude(), longitude: faker.location.longitude(), userId: randomUser.id, diff --git a/src/app.ts b/src/app.ts index 48cbf3d..aff71b5 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,15 +11,12 @@ export const app = fastify() app.register(fastifyCors, { origin: (origin, cb) => { - const allowedOrigins = [ - 'http://localhost:5500', - 'https://mlkp1.github.io/Front2/', - ] + const allowedOrigins = ['http://localhost:5500', 'https://mlkp1.github.io'] if (!origin || allowedOrigins.includes(origin)) { cb(null, true) return } - cb(new Error('Not allowed'), false) + cb(new Error(`Not allowed from origin ${origin}`), false) }, credentials: true, methods: '*', @@ -53,6 +50,10 @@ app.setErrorHandler((error, _, reply) => { .send({ message: 'Validation error.', issues: error.format() }) } + if (error.message.includes("Can't reach database server")) { + return reply.status(503).send({ message: 'Database error.' }) + } + if (env.NODE_ENV !== 'prod') { console.error(error) } else { diff --git a/src/http/controllers/address/getAddress.ts b/src/http/controllers/address/getAddress.ts new file mode 100644 index 0000000..d42c727 --- /dev/null +++ b/src/http/controllers/address/getAddress.ts @@ -0,0 +1,23 @@ +import type { FastifyReply, FastifyRequest } from 'fastify' + +import { AddressNotExistsError } from '@/services/errors/address-not-exists-error' +import { makeGetAddressService } from '@/services/factories/addresses/make-get-address-service' + +export async function getAddress(request: FastifyRequest, reply: FastifyReply) { + const userId = request.user.sub + + let address = null + try { + const getAddressService = makeGetAddressService() + + address = await getAddressService.execute({ userId }) + } catch (err) { + if (err instanceof AddressNotExistsError) { + return reply.status(404).send({ message: err.message }) + } + + throw err + } + + return reply.status(200).send(address) +} diff --git a/src/http/controllers/address/list.ts b/src/http/controllers/address/list.ts new file mode 100644 index 0000000..704c16d --- /dev/null +++ b/src/http/controllers/address/list.ts @@ -0,0 +1,17 @@ +import type { FastifyReply, FastifyRequest } from 'fastify' + +import { makeListService } from '@/services/factories/addresses/make-list-service' + +export async function list(_: FastifyRequest, reply: FastifyReply) { + let addresses = null + try { + const listService = makeListService() + + addresses = await listService.execute() + } catch (err) { + // biome-ignore lint/complexity/noUselessCatch: + throw err + } + + return reply.status(200).send({ addresses: addresses.address }) +} diff --git a/src/http/controllers/address/register.ts b/src/http/controllers/address/register.ts new file mode 100644 index 0000000..f3d952a --- /dev/null +++ b/src/http/controllers/address/register.ts @@ -0,0 +1,86 @@ +import type { FastifyReply, FastifyRequest } from 'fastify' +import { z } from 'zod' + +import { AddressAlreadyExistsError } from '@/services/errors/address-already-exists-error' +import { UserNotExistsError } from '@/services/errors/user-not-exists-error' +import { makeRegisterService } from '@/services/factories/addresses/make-register-service' +import { + type AddressApiResponse, + getAddressByZipCode, +} from '@/utils/get-address-by-zipcode' +import { validateAddressFromApi } from '@/utils/validate-address' + +export async function register(request: FastifyRequest, reply: FastifyReply) { + const registerBodySchema = z.object({ + number: z.number().int().nonnegative().min(100).max(99999), + street: z.string().nonempty(), + neighborhood: z.string().nonempty(), + complement: z.string().optional(), + city: z.string().nonempty(), + zipCode: z + .number() + .int() + .min(10000000) + .max(99999999) + .refine( + zip => { + const prefix = Number.parseInt(zip.toString().substring(0, 2)) + return prefix >= 1 && prefix <= 19 + }, + { + message: 'CEP não pertence ao estado de São Paulo', + }, + ), + latitude: z.number().min(-90).max(90).optional(), + longitude: z.number().min(-180).max(180).optional(), + }) + + const parsedBody = registerBodySchema.parse(request.body) + let { zipCode, latitude, longitude, street, neighborhood, city } = parsedBody + + let addressData: AddressApiResponse + try { + addressData = await getAddressByZipCode(zipCode) + } catch (err) { + return reply.status(400).send({ + message: (err as Error).message, + }) + } + + if (!latitude || !longitude) { + latitude = addressData.latitudeApi + longitude = addressData.longitudeApi + } + + try { + validateAddressFromApi( + { zipCode, street, neighborhood, city, latitude, longitude }, + addressData, + ) + } catch (err) { + return reply.status(400).send({ message: (err as Error).message }) + } + + try { + const registerService = makeRegisterService() + + await registerService.execute({ + ...parsedBody, + userId: request.user.sub, + latitude, + longitude, + }) + } catch (err) { + if (err instanceof UserNotExistsError) { + return reply.status(404).send({ message: err.message }) + } + + if (err instanceof AddressAlreadyExistsError) { + return reply.status(409).send({ message: err.message }) + } + + throw err + } + + return reply.status(201).send() +} diff --git a/src/http/controllers/address/remove.ts b/src/http/controllers/address/remove.ts new file mode 100644 index 0000000..3413b10 --- /dev/null +++ b/src/http/controllers/address/remove.ts @@ -0,0 +1,21 @@ +import type { FastifyReply, FastifyRequest } from 'fastify' + +import { AddressNotExistsError } from '@/services/errors/address-not-exists-error' +import { makeRemoveService } from '@/services/factories/addresses/make-remove-service' + +export async function remove(request: FastifyRequest, reply: FastifyReply) { + const userId = request.user.sub + try { + const removeService = makeRemoveService() + + await removeService.execute({ userId }) + } catch (err) { + if (err instanceof AddressNotExistsError) { + return reply.status(400).send({ message: err.message }) + } + + throw err + } + + return reply.status(204).send() +} diff --git a/src/http/controllers/address/routes.ts b/src/http/controllers/address/routes.ts new file mode 100644 index 0000000..f1fc52c --- /dev/null +++ b/src/http/controllers/address/routes.ts @@ -0,0 +1,19 @@ +import type { FastifyInstance } from 'fastify' + +import { verifyJWT } from '@/http/middlewares/verify-jwt' +import { verifyUserRole } from '@/http/middlewares/verify-user-role' + +import { getAddress } from './getAddress' +import { list } from './list' +import { register } from './register' +import { remove } from './remove' +import { update } from './update' + +export async function addressRoutes(app: FastifyInstance) { + app.post('/address', { onRequest: [verifyJWT] }, register) + app.get('/address', { onRequest: [verifyJWT] }, getAddress) + app.patch('/address', { onRequest: [verifyJWT] }, update) + app.delete('/address', { onRequest: [verifyJWT] }, remove) + + app.get('/addresses', { onRequest: [verifyUserRole('ADMIN')] }, list) +} diff --git a/src/http/controllers/address/update.ts b/src/http/controllers/address/update.ts new file mode 100644 index 0000000..b1e5bfd --- /dev/null +++ b/src/http/controllers/address/update.ts @@ -0,0 +1,79 @@ +import type { FastifyReply, FastifyRequest } from 'fastify' +import { z } from 'zod' + +import { AddressNotExistsError } from '@/services/errors/address-not-exists-error' +import { makeUpdateService } from '@/services/factories/addresses/make-update-service' +import { getAddressByZipCode } from '@/utils/get-address-by-zipcode' + +export async function update(request: FastifyRequest, reply: FastifyReply) { + const updateBodySchema = z + .object({ + zipCode: z + .number() + .int() + .min(10000000) + .max(99999999) + .refine( + zip => { + const prefix = Number.parseInt(zip.toString().substring(0, 2)) + return prefix >= 1 && prefix <= 19 + }, + { + message: 'CEP não pertence ao estado de São Paulo', + }, + ) + .optional(), + number: z.number().int().nonnegative().min(100).max(99999).optional(), + complement: z.string().optional(), + }) + .refine( + data => { + if (data.zipCode !== undefined && data.number === undefined) { + return false + } + + return ( + data.zipCode !== undefined || + data.number !== undefined || + data.complement !== undefined + ) + }, + { + message: + 'Se zipCode for fornecido, number também deve ser fornecido. Pelo menos um campo deve ser fornecido.', + }, + ) + + const userId = request.user.sub + const parsedBody = updateBodySchema.parse(request.body) + const { zipCode } = parsedBody + + let addressApi = null + try { + if (zipCode) { + addressApi = await getAddressByZipCode(zipCode) + } + } catch (err) { + return reply.status(400).send({ + message: (err as Error).message, + }) + } + + let address = null + try { + const updateService = makeUpdateService() + + address = await updateService.execute({ + userId, + data: { ...parsedBody, addressApi }, + }) + } catch (err) { + if (err instanceof AddressNotExistsError) { + return reply.status(404).send({ message: err.message }) + } + + throw err + } + + return reply.status(200).send(address) +} diff --git a/src/http/controllers/routes.ts b/src/http/controllers/routes.ts index 99fc169..c942564 100644 --- a/src/http/controllers/routes.ts +++ b/src/http/controllers/routes.ts @@ -1,7 +1,9 @@ import type { FastifyInstance } from 'fastify' +import { addressRoutes } from './address/routes' import { usersRoutes } from './users/routes' export async function appRoutes(app: FastifyInstance) { app.register(usersRoutes) + app.register(addressRoutes) } diff --git a/src/http/controllers/users/authenticate.ts b/src/http/controllers/users/authenticate.ts index 38c57e6..8f19a38 100644 --- a/src/http/controllers/users/authenticate.ts +++ b/src/http/controllers/users/authenticate.ts @@ -2,7 +2,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify' import { z } from 'zod' import { InvalidCredentialsError } from '@/services/errors/invalid-credentials-error' -import { makeAuthenticateService } from '@/services/factories/make-authenticate-service' +import { makeAuthenticateService } from '@/services/factories/users/make-authenticate-service' export async function authenticate( request: FastifyRequest, diff --git a/src/http/controllers/users/getUser.ts b/src/http/controllers/users/getUser.ts index e8008ef..56d55ae 100644 --- a/src/http/controllers/users/getUser.ts +++ b/src/http/controllers/users/getUser.ts @@ -1,6 +1,6 @@ import type { FastifyReply, FastifyRequest } from 'fastify' -import { makeGetUserService } from '@/services/factories/make-get-user-service' +import { makeGetUserService } from '@/services/factories/users/make-get-user-service' export async function getUser(request: FastifyRequest, reply: FastifyReply) { const getUser = makeGetUserService() diff --git a/src/http/controllers/users/list.ts b/src/http/controllers/users/list.ts index 123f860..64935d6 100644 --- a/src/http/controllers/users/list.ts +++ b/src/http/controllers/users/list.ts @@ -1,7 +1,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify' import { UserNotExistsError } from '@/services/errors/user-not-exists-error' -import { makeListService } from '@/services/factories/make-list-service' +import { makeListService } from '@/services/factories/users/make-list-service' export async function list(_: FastifyRequest, reply: FastifyReply) { let users = null diff --git a/src/http/controllers/users/register.ts b/src/http/controllers/users/register.ts index 5d78ecc..ed6aa9f 100644 --- a/src/http/controllers/users/register.ts +++ b/src/http/controllers/users/register.ts @@ -2,7 +2,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify' import { z } from 'zod' import { UserAlreadyExistsError } from '@/services/errors/user-already-exists-error' -import { makeRegisterService } from '@/services/factories/make-register-service' +import { makeRegisterService } from '@/services/factories/users/make-register-service' export async function register(request: FastifyRequest, reply: FastifyReply) { const registerBodySchema = z.object({ diff --git a/src/http/controllers/users/remove.ts b/src/http/controllers/users/remove.ts index b774e3c..b171749 100644 --- a/src/http/controllers/users/remove.ts +++ b/src/http/controllers/users/remove.ts @@ -1,7 +1,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify' import { UserNotExistsError } from '@/services/errors/user-not-exists-error' -import { makeRemoveService } from '@/services/factories/make-remove-service' +import { makeRemoveService } from '@/services/factories/users/make-remove-service' import { logout } from './logout' diff --git a/src/http/controllers/users/update.ts b/src/http/controllers/users/update.ts index 1dd9e91..28f44fb 100644 --- a/src/http/controllers/users/update.ts +++ b/src/http/controllers/users/update.ts @@ -2,7 +2,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify' import { z } from 'zod' import { UserNotExistsError } from '@/services/errors/user-not-exists-error' -import { makeUpdateService } from '@/services/factories/make-update-service' +import { makeUpdateService } from '@/services/factories/users/make-update-service' export async function update(request: FastifyRequest, reply: FastifyReply) { const updateBodySchema = z diff --git a/src/repositories/address-repository.ts b/src/repositories/address-repository.ts new file mode 100644 index 0000000..e212698 --- /dev/null +++ b/src/repositories/address-repository.ts @@ -0,0 +1,12 @@ +import type { Address, Prisma } from '@prisma/client' + +export interface AddressRepository { + create(userId: string, data: Prisma.AddressCreateInput): Promise
+ findByUserId(userId: string): Promise
+ updateByUserId( + userId: string, + data: Prisma.AddressUpdateInput, + ): Promise
+ removeByUserId(userId: string): Promise
+ list(): Promise +} diff --git a/src/repositories/prisma/prisma-address-repository.ts b/src/repositories/prisma/prisma-address-repository.ts new file mode 100644 index 0000000..5a5a2b7 --- /dev/null +++ b/src/repositories/prisma/prisma-address-repository.ts @@ -0,0 +1,58 @@ +import { prisma } from '@/lib/prisma' +import type { Prisma } from '@prisma/client' + +export class PrismaAddressRepository { + async create(userId: string, data: Prisma.AddressCreateInput) { + const address = await prisma.address.create({ + data: { + ...data, + user: { + connect: { id: userId }, + }, + }, + }) + + return address + } + + async findByUserId(userId: string) { + const address = await prisma.address.findFirst({ + where: { + userId, + }, + }) + + return address + } + + async updateByUserId(userId: string, data: Prisma.AddressUpdateInput) { + const address = await prisma.address.update({ + where: { + userId, + }, + data, + }) + + return address + } + + async list() { + const address = await prisma.address.findMany({ + orderBy: { + updatedAt: 'desc', + }, + }) + + return address + } + + async removeByUserId(userId: string) { + const address = await prisma.address.delete({ + where: { + userId, + }, + }) + + return address + } +} diff --git a/src/services/addresses/get-address.ts b/src/services/addresses/get-address.ts new file mode 100644 index 0000000..4ed0d34 --- /dev/null +++ b/src/services/addresses/get-address.ts @@ -0,0 +1,20 @@ +import type { AddressRepository } from '@/repositories/address-repository' +import { AddressNotExistsError } from '../errors/address-not-exists-error' + +interface GetAddressServiceRequest { + userId: string +} + +export class GetAddressService { + constructor(private addressRepository: AddressRepository) {} + + async execute({ userId }: GetAddressServiceRequest) { + const address = await this.addressRepository.findByUserId(userId) + + if (!address) { + throw new AddressNotExistsError() + } + + return { address } + } +} diff --git a/src/services/addresses/list.ts b/src/services/addresses/list.ts new file mode 100644 index 0000000..38bf61d --- /dev/null +++ b/src/services/addresses/list.ts @@ -0,0 +1,11 @@ +import type { AddressRepository } from '@/repositories/address-repository' + +export class ListService { + constructor(private addressRepository: AddressRepository) {} + + async execute() { + const address = await this.addressRepository.list() + + return { address } + } +} diff --git a/src/services/addresses/register.ts b/src/services/addresses/register.ts new file mode 100644 index 0000000..ad44642 --- /dev/null +++ b/src/services/addresses/register.ts @@ -0,0 +1,73 @@ +import type { AddressRepository } from '@/repositories/address-repository' +import type { UsersRepository } from '@/repositories/users-repository' +import { AddressAlreadyExistsError } from '../errors/address-already-exists-error' +import { UserNotExistsError } from '../errors/user-not-exists-error' + +interface RegisterServiceRequest { + number: number + street: string + neighborhood: string + complement?: string + city: string + state?: string + country?: string + zipCode: number + latitude: number + longitude: number + + userId: string +} + +export class RegisterService { + constructor( + private addressRepository: AddressRepository, + private userRepository: UsersRepository, + ) {} + + async execute({ + number, + street, + neighborhood, + complement, + city, + state, + country, + zipCode, + latitude, + longitude, + userId, + }: RegisterServiceRequest) { + const doesUserExists = await this.userRepository.findById(userId) + + if (!doesUserExists) { + throw new UserNotExistsError() + } + + const doesAddressAlreadyExists = + await this.addressRepository.findByUserId(userId) + + if (doesAddressAlreadyExists) { + throw new AddressAlreadyExistsError() + } + + const address = await this.addressRepository.create(userId, { + number, + street, + neighborhood, + complement, + city, + state, + country, + zipCode, + latitude, + longitude, + user: { + connect: { + id: userId, + }, + }, + }) + + return { address } + } +} diff --git a/src/services/addresses/remove.ts b/src/services/addresses/remove.ts new file mode 100644 index 0000000..2c7a354 --- /dev/null +++ b/src/services/addresses/remove.ts @@ -0,0 +1,22 @@ +import type { AddressRepository } from '@/repositories/address-repository' +import { AddressNotExistsError } from '../errors/address-not-exists-error' + +interface RemoveServiceRequest { + userId: string +} + +export class RemoveService { + constructor(private addressRepository: AddressRepository) {} + + async execute({ userId }: RemoveServiceRequest) { + const doesAddressExists = await this.addressRepository.findByUserId(userId) + + if (!doesAddressExists) { + throw new AddressNotExistsError() + } + + const address = await this.addressRepository.removeByUserId(userId) + + return { address } + } +} diff --git a/src/services/addresses/update.ts b/src/services/addresses/update.ts new file mode 100644 index 0000000..5f9b678 --- /dev/null +++ b/src/services/addresses/update.ts @@ -0,0 +1,48 @@ +import type { AddressRepository } from '@/repositories/address-repository' +import type { AddressApiResponse } from '@/utils/get-address-by-zipcode' +import { AddressNotExistsError } from '../errors/address-not-exists-error' + +interface UpdateServiceRequest { + userId: string + data: { + zipCode?: number + number?: number + complement?: string + addressApi: AddressApiResponse | null + } +} + +export class UpdateService { + constructor(private addressRepository: AddressRepository) {} + + async execute({ userId, data }: UpdateServiceRequest) { + const doesAddressExists = await this.addressRepository.findByUserId(userId) + + if (!doesAddressExists) { + throw new AddressNotExistsError() + } + + let address = null + if (data.zipCode !== undefined && data.addressApi !== null) { + address = await this.addressRepository.updateByUserId(userId, { + zipCode: data.zipCode, + number: data.number, + complement: data.complement, + neighborhood: data.addressApi.neighborhoodApi, + street: data.addressApi.streetApi, + city: data.addressApi.cityApi, + latitude: data.addressApi.latitudeApi, + longitude: data.addressApi.longitudeApi, + }) + } + + if (data.zipCode === undefined) { + address = await this.addressRepository.updateByUserId(userId, { + number: data.number, + complement: data.complement, + }) + } + + return { address } + } +} diff --git a/src/services/errors/address-already-exists-error.ts b/src/services/errors/address-already-exists-error.ts new file mode 100644 index 0000000..26dab77 --- /dev/null +++ b/src/services/errors/address-already-exists-error.ts @@ -0,0 +1,5 @@ +export class AddressAlreadyExistsError extends Error { + constructor() { + super('Address already exists.') + } +} diff --git a/src/services/errors/address-not-exists-error.ts b/src/services/errors/address-not-exists-error.ts new file mode 100644 index 0000000..03d9a82 --- /dev/null +++ b/src/services/errors/address-not-exists-error.ts @@ -0,0 +1,5 @@ +export class AddressNotExistsError extends Error { + constructor() { + super('Address not exists.') + } +} diff --git a/src/services/factories/addresses/make-get-address-service.ts b/src/services/factories/addresses/make-get-address-service.ts new file mode 100644 index 0000000..9b09a10 --- /dev/null +++ b/src/services/factories/addresses/make-get-address-service.ts @@ -0,0 +1,9 @@ +import { PrismaAddressRepository } from '@/repositories/prisma/prisma-address-repository' +import { GetAddressService } from '@/services/addresses/get-address' + +export function makeGetAddressService() { + const addressRepository = new PrismaAddressRepository() + const getAddressService = new GetAddressService(addressRepository) + + return getAddressService +} diff --git a/src/services/factories/addresses/make-list-service.ts b/src/services/factories/addresses/make-list-service.ts new file mode 100644 index 0000000..9677010 --- /dev/null +++ b/src/services/factories/addresses/make-list-service.ts @@ -0,0 +1,9 @@ +import { PrismaAddressRepository } from '@/repositories/prisma/prisma-address-repository' +import { ListService } from '@/services/addresses/list' + +export function makeListService() { + const addressRepository = new PrismaAddressRepository() + const listService = new ListService(addressRepository) + + return listService +} diff --git a/src/services/factories/addresses/make-register-service.ts b/src/services/factories/addresses/make-register-service.ts new file mode 100644 index 0000000..edbae20 --- /dev/null +++ b/src/services/factories/addresses/make-register-service.ts @@ -0,0 +1,15 @@ +import { PrismaAddressRepository } from '@/repositories/prisma/prisma-address-repository' +import { PrismaUsersRepository } from '@/repositories/prisma/prisma-users-repository' +import { RegisterService } from '@/services/addresses/register' + +export function makeRegisterService() { + const addressRepository = new PrismaAddressRepository() + const usersRepository = new PrismaUsersRepository() + + const registerService = new RegisterService( + addressRepository, + usersRepository, + ) + + return registerService +} diff --git a/src/services/factories/addresses/make-remove-service.ts b/src/services/factories/addresses/make-remove-service.ts new file mode 100644 index 0000000..36888c7 --- /dev/null +++ b/src/services/factories/addresses/make-remove-service.ts @@ -0,0 +1,9 @@ +import { PrismaAddressRepository } from '@/repositories/prisma/prisma-address-repository' +import { RemoveService } from '@/services/addresses/remove' + +export function makeRemoveService() { + const addressRepository = new PrismaAddressRepository() + const removeService = new RemoveService(addressRepository) + + return removeService +} diff --git a/src/services/factories/addresses/make-update-service.ts b/src/services/factories/addresses/make-update-service.ts new file mode 100644 index 0000000..0d4c8b0 --- /dev/null +++ b/src/services/factories/addresses/make-update-service.ts @@ -0,0 +1,9 @@ +import { PrismaAddressRepository } from '@/repositories/prisma/prisma-address-repository' +import { UpdateService } from '@/services/addresses/update' + +export function makeUpdateService() { + const addressRepository = new PrismaAddressRepository() + const updateService = new UpdateService(addressRepository) + + return updateService +} diff --git a/src/services/factories/make-authenticate-service.ts b/src/services/factories/users/make-authenticate-service.ts similarity index 82% rename from src/services/factories/make-authenticate-service.ts rename to src/services/factories/users/make-authenticate-service.ts index 39e288e..9fe52f1 100644 --- a/src/services/factories/make-authenticate-service.ts +++ b/src/services/factories/users/make-authenticate-service.ts @@ -1,5 +1,5 @@ import { PrismaUsersRepository } from '@/repositories/prisma/prisma-users-repository' -import { AuthenticateService } from '../users/authenticate' +import { AuthenticateService } from '../../users/authenticate' export function makeAuthenticateService() { const usersRepository = new PrismaUsersRepository() diff --git a/src/services/factories/make-get-user-service.ts b/src/services/factories/users/make-get-user-service.ts similarity index 100% rename from src/services/factories/make-get-user-service.ts rename to src/services/factories/users/make-get-user-service.ts diff --git a/src/services/factories/make-list-service.ts b/src/services/factories/users/make-list-service.ts similarity index 100% rename from src/services/factories/make-list-service.ts rename to src/services/factories/users/make-list-service.ts diff --git a/src/services/factories/make-register-service.ts b/src/services/factories/users/make-register-service.ts similarity index 83% rename from src/services/factories/make-register-service.ts rename to src/services/factories/users/make-register-service.ts index 7505741..a3b1395 100644 --- a/src/services/factories/make-register-service.ts +++ b/src/services/factories/users/make-register-service.ts @@ -1,5 +1,5 @@ import { PrismaUsersRepository } from '@/repositories/prisma/prisma-users-repository' -import { RegisterService } from '../users/register' +import { RegisterService } from '../../users/register' export function makeRegisterService() { const usersRepository = new PrismaUsersRepository() diff --git a/src/services/factories/make-remove-service.ts b/src/services/factories/users/make-remove-service.ts similarity index 83% rename from src/services/factories/make-remove-service.ts rename to src/services/factories/users/make-remove-service.ts index 921b66e..d5b33ae 100644 --- a/src/services/factories/make-remove-service.ts +++ b/src/services/factories/users/make-remove-service.ts @@ -1,5 +1,5 @@ import { PrismaUsersRepository } from '@/repositories/prisma/prisma-users-repository' -import { RemoveService } from '../users/remove' +import { RemoveService } from '../../users/remove' export function makeRemoveService() { const usersRepository = new PrismaUsersRepository() diff --git a/src/services/factories/make-update-service.ts b/src/services/factories/users/make-update-service.ts similarity index 100% rename from src/services/factories/make-update-service.ts rename to src/services/factories/users/make-update-service.ts diff --git a/src/utils/get-address-by-zipcode.ts b/src/utils/get-address-by-zipcode.ts new file mode 100644 index 0000000..fd5d65c --- /dev/null +++ b/src/utils/get-address-by-zipcode.ts @@ -0,0 +1,70 @@ +interface BrazilApiResponse { + cep: number + state: string + city: string + neighborhood: string + street: string + service: string + location: { + type: string + coordinates: { + latitude: number + longitude: number + } + } +} + +export interface AddressApiResponse { + latitudeApi: number + longitudeApi: number + streetApi: string + neighborhoodApi: string + cityApi: string + zipCodeApi: number +} + +export async function getAddressByZipCode( + zipCode: number, +): Promise { + let data: BrazilApiResponse + try { + const response = await fetch( + `https://brasilapi.com.br/api/cep/v2/${zipCode}`, + { + signal: AbortSignal.timeout(5000), + }, + ) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + data = await response.json() + } catch (err) { + throw new Error( + `Erro ao buscar dados do endereço: ${(err as Error).message}`, + ) + } + + const zipCodeApi = Number(data.cep) + const streetApi = data.street + const neighborhoodApi = data.neighborhood + const cityApi = data.city + const latitudeApi = Number(data.location.coordinates.latitude) + const longitudeApi = Number(data.location.coordinates.longitude) + + if (!latitudeApi || !longitudeApi) { + throw new Error( + 'Não foi possível obter as coordenadas para o CEP informado.', + ) + } + + return { + zipCodeApi, + streetApi, + neighborhoodApi, + cityApi, + latitudeApi, + longitudeApi, + } +} diff --git a/src/utils/validate-address.ts b/src/utils/validate-address.ts new file mode 100644 index 0000000..d4b2443 --- /dev/null +++ b/src/utils/validate-address.ts @@ -0,0 +1,41 @@ +import type { AddressApiResponse } from './get-address-by-zipcode' + +interface AddressUser { + street: string + neighborhood: string + city: string + zipCode: number + latitude: number + longitude: number +} + +const errorMessage = + 'Os dados fornecidos não correspondem ao endereço retornado pela API.' + +export function validateAddressFromApi( + { latitude, longitude, street, neighborhood, city, zipCode }: AddressUser, + { + zipCodeApi, + streetApi, + neighborhoodApi, + cityApi, + latitudeApi, + longitudeApi, + }: AddressApiResponse, +) { + if ( + zipCodeApi !== zipCode || + latitudeApi !== latitude || + longitudeApi !== longitude + ) { + throw new Error(errorMessage) + } + + if ( + streetApi.toLowerCase() !== street.toLowerCase() || + neighborhoodApi.toLowerCase() !== neighborhood.toLowerCase() || + cityApi.toLowerCase() !== city.toLowerCase() + ) { + throw new Error(errorMessage) + } +}