diff --git a/src/config/prisma.ts b/src/config/prisma.ts index 90bc691..2d72fee 100644 --- a/src/config/prisma.ts +++ b/src/config/prisma.ts @@ -27,4 +27,4 @@ if (process.env.NODE_ENV !== "production") { globalThis.prisma = prisma; } -export { prisma }; +export default { prisma }; diff --git a/src/modules/auth/presentation/controllers/Auth.controller.disabled b/src/modules/auth/presentation/controllers/Auth.controller.disabled deleted file mode 100644 index 797fba6..0000000 --- a/src/modules/auth/presentation/controllers/Auth.controller.disabled +++ /dev/null @@ -1,169 +0,0 @@ -import { Request, Response } from "express"; -import AuthService from "../../../../services/AuthService"; -import { AuthenticatedRequest } from "../../../../types/auth.types"; - -class AuthController { - private authService: AuthService; - - constructor() { - this.authService = new AuthService(); - } - - register = async (req: Request, res: Response): Promise => { - const { name, lastName, email, password, wallet } = req.body; - - try { - const response = await this.authService.register( - name, - lastName, - email, - password, - wallet - ); - res.status(201).json(response); - } catch (error) { - res.status(400).json({ - message: error instanceof Error ? error.message : "Registration failed", - }); - } - }; - - verifyEmail = async (req: Request, res: Response): Promise => { - const token = - typeof req.params.token === "string" - ? req.params.token - : typeof req.query.token === "string" - ? req.query.token - : undefined; - - if (!token || typeof token !== "string") { - res.status(400).json({ message: "Token is required" }); - return; - } - - try { - const response = await this.authService.verifyEmail(token); - res.json(response); - } catch (error) { - res.status(400).json({ - message: error instanceof Error ? error.message : "Verification failed", - }); - } - }; - - resendVerificationEmail = async ( - req: Request, - res: Response - ): Promise => { - const { email } = req.body; - - if (!email) { - res.status(400).json({ message: "Email is required" }); - return; - } - - try { - const response = await this.authService.resendVerificationEmail(email); - res.json(response); - } catch (error) { - res.status(400).json({ - message: - error instanceof Error ? error.message : "Could not resend email", - }); - } - }; - - login = async (req: Request, res: Response): Promise => { - const { walletAddress } = req.body; - - try { - const token = await this.authService.authenticate(walletAddress); - res.json({ token }); - } catch (error) { - res.status(401).json({ - message: error instanceof Error ? error.message : "Unknown error", - }); - } - }; - - checkVerificationStatus = async ( - req: AuthenticatedRequest, - res: Response - ): Promise => { - if (!req.user) { - res.status(401).json({ message: "User not authenticated" }); - return; - } - - try { - const status = await this.authService.checkVerificationStatus( - req.user.id.toString() - ); - res.json(status); - } catch (error) { - res.status(400).json({ - message: - error instanceof Error - ? error.message - : "Could not check verification status", - }); - } - }; - - protectedRoute = (req: AuthenticatedRequest, res: Response): void => { - if (!req.user) { - res.status(401).json({ message: "User not authenticated" }); - return; - } - - res.json({ - message: `Hello ${req.user.role}`, - userId: req.user.id, - isVerified: req.user.isVerified, - }); - }; - - verifyWallet = async (req: Request, res: Response): Promise => { - const { walletAddress } = req.body; - - if (!walletAddress) { - res.status(400).json({ message: "Wallet address is required" }); - return; - } - - try { - const verification = - await this.authService.verifyWalletAddress(walletAddress); - res.json(verification); - } catch (error) { - res.status(400).json({ - message: - error instanceof Error ? error.message : "Wallet verification failed", - }); - } - }; - - validateWalletFormat = async (req: Request, res: Response): Promise => { - const { walletAddress } = req.body; - - if (!walletAddress) { - res.status(400).json({ message: "Wallet address is required" }); - return; - } - - try { - const validation = - await this.authService.validateWalletFormat(walletAddress); - res.json(validation); - } catch (error) { - res.status(400).json({ - message: - error instanceof Error - ? error.message - : "Wallet format validation failed", - }); - } - }; -} - -export default new AuthController(); diff --git a/src/modules/organization/README.md b/src/modules/organization/README.md index a6a21c0..548a538 100644 --- a/src/modules/organization/README.md +++ b/src/modules/organization/README.md @@ -4,6 +4,18 @@ The Organization module manages all organization-related operations including creation, updates, verification, and settings management. This module handles the business logic for organizations that post volunteer opportunities on the VolunChain platform. +## Patrón de Inyección de Dependencias + +El módulo utiliza un patrón de inyección de dependencias para desacoplar los componentes y facilitar las pruebas unitarias: + +1. **Repositorios**: Implementan interfaces definidas en el dominio y reciben dependencias externas (como PrismaClient) a través del constructor. + +2. **Casos de Uso**: Reciben el repositorio como dependencia a través del constructor, lo que permite intercambiar implementaciones fácilmente. + +3. **Controladores**: Reciben todos los casos de uso necesarios a través del constructor. + +4. **Módulo**: La clase `OrganizationModule` actúa como un contenedor de dependencias que inicializa y conecta todos los componentes. + ## Architecture ### Domain Layer (`domain/`) @@ -34,6 +46,18 @@ The Organization module manages all organization-related operations including cr ## Development +### Using the Module + +Para utilizar este módulo en otras partes de la aplicación: + +```typescript +// Inicializar el módulo (normalmente en la configuración de la aplicación) +OrganizationModule.initialize(); + +// Obtener el controlador con todas sus dependencias inyectadas +const organizationController = OrganizationModule.getController(); +``` + ### Adding New Features 1. **Domain Changes** diff --git a/src/modules/organization/application/repository/organization.repository.ts b/src/modules/organization/application/repository/organization.repository.ts new file mode 100644 index 0000000..c6c6c4e --- /dev/null +++ b/src/modules/organization/application/repository/organization.repository.ts @@ -0,0 +1,22 @@ +import { OrganizationEntity } from "../../domain/entities/organization.entity"; + +export interface IOrganizationRepository { + save(organization: OrganizationEntity): Promise; + findById(id: string): Promise; + findByEmail(email: string): Promise; + findAll(options?: { + page?: number; + limit?: number; + search?: string; + }): Promise; + findAll( + page?: number, + limit?: number, + search?: string + ): Promise; + update( + id: string, + organization: OrganizationEntity + ): Promise; + delete(id: string): Promise; +} diff --git a/src/modules/organization/application/use-cases/create-organization.usecase.ts b/src/modules/organization/application/use-cases/create-organization.use-case.ts similarity index 72% rename from src/modules/organization/application/use-cases/create-organization.usecase.ts rename to src/modules/organization/application/use-cases/create-organization.use-case.ts index 560d4aa..b032489 100644 --- a/src/modules/organization/application/use-cases/create-organization.usecase.ts +++ b/src/modules/organization/application/use-cases/create-organization.use-case.ts @@ -1,6 +1,6 @@ import { CreateOrganizationDto } from "../../presentation/dto/create-organization.dto"; -import { Organization } from "../../domain/entities/organization.entity"; -import { IOrganizationRepository } from "../../domain/interfaces/organization-repository.interface"; +import { OrganizationEntity } from "../../domain/entities/organization.entity"; +import { IOrganizationRepository } from "../../application/repository/organization.repository"; import { randomUUID } from "crypto"; export class CreateOrganizationUseCase { @@ -8,7 +8,7 @@ export class CreateOrganizationUseCase { private readonly organizationRepository: IOrganizationRepository ) {} - async execute(dto: CreateOrganizationDto): Promise { + async execute(dto: CreateOrganizationDto): Promise { const organizationProps = { id: randomUUID(), name: dto.name, @@ -21,7 +21,7 @@ export class CreateOrganizationUseCase { isVerified: false, }; - const organization = Organization.create(organizationProps); + const organization = OrganizationEntity.create(organizationProps); return await this.organizationRepository.save(organization); } diff --git a/src/modules/organization/application/use-cases/delete-organization.usecase.ts b/src/modules/organization/application/use-cases/delete-organization.use-case.ts similarity index 83% rename from src/modules/organization/application/use-cases/delete-organization.usecase.ts rename to src/modules/organization/application/use-cases/delete-organization.use-case.ts index 2302874..e1ddca7 100644 --- a/src/modules/organization/application/use-cases/delete-organization.usecase.ts +++ b/src/modules/organization/application/use-cases/delete-organization.use-case.ts @@ -1,4 +1,4 @@ -import { IOrganizationRepository } from "../../domain/interfaces/organization-repository.interface"; +import { IOrganizationRepository } from "../repository/organization.repository"; import { OrganizationNotFoundException } from "../../domain/exceptions/organization-not-found.exception"; export class DeleteOrganizationUseCase { diff --git a/src/modules/organization/application/use-cases/get-all-organizations.usecase.ts b/src/modules/organization/application/use-cases/get-all-organizations.use-case.ts similarity index 59% rename from src/modules/organization/application/use-cases/get-all-organizations.usecase.ts rename to src/modules/organization/application/use-cases/get-all-organizations.use-case.ts index 0f127cb..72d16c7 100644 --- a/src/modules/organization/application/use-cases/get-all-organizations.usecase.ts +++ b/src/modules/organization/application/use-cases/get-all-organizations.use-case.ts @@ -1,5 +1,5 @@ -import { Organization } from "../../domain/entities/organization.entity"; -import { IOrganizationRepository } from "../../domain/interfaces/organization-repository.interface"; +import { OrganizationEntity } from "../../domain/entities/organization.entity"; +import { IOrganizationRepository } from "../repository/organization.repository"; interface GetAllOrganizationsOptions { page: number; @@ -12,7 +12,9 @@ export class GetAllOrganizationsUseCase { private readonly organizationRepository: IOrganizationRepository ) {} - async execute(options: GetAllOrganizationsOptions): Promise { + async execute( + options: GetAllOrganizationsOptions + ): Promise { return await this.organizationRepository.findAll({ page: options.page, limit: options.limit, diff --git a/src/modules/organization/application/use-cases/get-organization-by-id.usecase.ts b/src/modules/organization/application/use-cases/get-organization-by-id.use-case.ts similarity index 65% rename from src/modules/organization/application/use-cases/get-organization-by-id.usecase.ts rename to src/modules/organization/application/use-cases/get-organization-by-id.use-case.ts index 9a1b8cd..5b413f8 100644 --- a/src/modules/organization/application/use-cases/get-organization-by-id.usecase.ts +++ b/src/modules/organization/application/use-cases/get-organization-by-id.use-case.ts @@ -1,5 +1,5 @@ -import { Organization } from "../../domain/entities/organization.entity"; -import { IOrganizationRepository } from "../../domain/interfaces/organization-repository.interface"; +import { OrganizationEntity } from "../../domain/entities/organization.entity"; +import { IOrganizationRepository } from "../repository/organization.repository"; import { OrganizationNotFoundException } from "../../domain/exceptions/organization-not-found.exception"; export class GetOrganizationByIdUseCase { @@ -7,7 +7,7 @@ export class GetOrganizationByIdUseCase { private readonly organizationRepository: IOrganizationRepository ) {} - async execute(id: string): Promise { + async execute(id: string): Promise { const organization = await this.organizationRepository.findById(id); if (!organization) { diff --git a/src/modules/organization/application/use-cases/index.ts b/src/modules/organization/application/use-cases/index.ts new file mode 100644 index 0000000..3a7766a --- /dev/null +++ b/src/modules/organization/application/use-cases/index.ts @@ -0,0 +1,5 @@ +export { DeleteOrganizationUseCase } from "./delete-organization.use-case"; +export { GetAllOrganizationsUseCase } from "./get-all-organizations.use-case"; +export { GetOrganizationByIdUseCase } from "./get-organization-by-id.use-case"; +export { UpdateOrganizationUseCase } from "./update-organization.use-case"; +export { CreateOrganizationUseCase } from "./create-organization.use-case"; diff --git a/src/modules/organization/application/use-cases/update-organization.usecase.ts b/src/modules/organization/application/use-cases/update-organization.use-case.ts similarity index 71% rename from src/modules/organization/application/use-cases/update-organization.usecase.ts rename to src/modules/organization/application/use-cases/update-organization.use-case.ts index 32b9eab..f12309f 100644 --- a/src/modules/organization/application/use-cases/update-organization.usecase.ts +++ b/src/modules/organization/application/use-cases/update-organization.use-case.ts @@ -1,6 +1,6 @@ import { UpdateOrganizationDto } from "../../presentation/dto/update-organization.dto"; -import { Organization } from "../../domain/entities/organization.entity"; -import { IOrganizationRepository } from "../../domain/interfaces/organization-repository.interface"; +import { OrganizationEntity } from "../../domain/entities/organization.entity"; +import { IOrganizationRepository } from "../repository/organization.repository"; import { OrganizationNotFoundException } from "../../domain/exceptions/organization-not-found.exception"; export class UpdateOrganizationUseCase { @@ -8,7 +8,10 @@ export class UpdateOrganizationUseCase { private readonly organizationRepository: IOrganizationRepository ) {} - async execute(id: string, dto: UpdateOrganizationDto): Promise { + async execute( + id: string, + dto: UpdateOrganizationDto + ): Promise { const existingOrganization = await this.organizationRepository.findById(id); if (!existingOrganization) { diff --git a/src/modules/organization/domain/entities/organization.entity.ts b/src/modules/organization/domain/entities/organization.entity.ts index e193e70..6156a8b 100644 --- a/src/modules/organization/domain/entities/organization.entity.ts +++ b/src/modules/organization/domain/entities/organization.entity.ts @@ -1,7 +1,7 @@ import { BaseEntity } from "../../../shared/domain/entities/base.entity"; +import { DomainException } from "@/modules/shared/domain/exceptions/domain.exception"; export interface OrganizationProps { - id: string; name: string; email: string; description: string; @@ -14,7 +14,13 @@ export interface OrganizationProps { walletAddress?: string; } -export class Organization extends BaseEntity { +export class InvalidOrganizationDataException extends DomainException { + constructor(field: string, value: string) { + super(`Invalid ${field}: ${value}`); + } +} + +export class OrganizationEntity extends BaseEntity { public readonly name: string; public readonly email: string; public readonly description: string; @@ -32,7 +38,48 @@ export class Organization extends BaseEntity { createdAt?: Date, updatedAt?: Date ) { - super(); + super(); // Llamar super() aunque BaseEntity no tenga constructor + + // Asignar propiedades de BaseEntity si se proporcionan + if (id) this.id = id; + if (createdAt) this.createdAt = createdAt; + if (updatedAt) this.updatedAt = updatedAt; + + // Validaciones de dominio usando DomainExceptions + if (!props.name?.trim()) { + throw new InvalidOrganizationDataException("name", "cannot be empty"); + } + + if (!props.email?.trim()) { + throw new InvalidOrganizationDataException("email", "cannot be empty"); + } + + if (!this.isValidEmail(props.email)) { + throw new InvalidOrganizationDataException("email", "invalid format"); + } + + if (!props.description?.trim()) { + throw new InvalidOrganizationDataException( + "description", + "cannot be empty" + ); + } + + if (props.name.length < 2) { + throw new InvalidOrganizationDataException( + "name", + "must be at least 2 characters" + ); + } + + if (props.description.length < 10) { + throw new InvalidOrganizationDataException( + "description", + "must be at least 10 characters" + ); + } + + // Asignar propiedades después de validar this.name = props.name; this.email = props.email; this.description = props.description; @@ -45,14 +92,16 @@ export class Organization extends BaseEntity { this.walletAddress = props.walletAddress; } - public static create(props: OrganizationProps, id?: string): Organization { - return new Organization(props, id); + public static create( + props: OrganizationProps, + id?: string + ): OrganizationEntity { + return new OrganizationEntity(props, id); } - public update(props: Partial): Organization { - return new Organization( + public update(props: Partial): OrganizationEntity { + return new OrganizationEntity( { - id: this.id, name: props.name ?? this.name, email: props.email ?? this.email, description: props.description ?? this.description, @@ -69,4 +118,55 @@ export class Organization extends BaseEntity { new Date() ); } + + // Método privado para validar formato de email + private isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } + + // Métodos de negocio + public verify(): OrganizationEntity { + return this.update({ isVerified: true }); + } + + public changeCategory(category: string): OrganizationEntity { + if (!category?.trim()) { + throw new InvalidOrganizationDataException("category", "cannot be empty"); + } + return this.update({ category }); + } + + public updateLogo(logoUrl: string): OrganizationEntity { + if (!logoUrl?.trim()) { + throw new InvalidOrganizationDataException("logoUrl", "cannot be empty"); + } + if (!this.isValidUrl(logoUrl)) { + throw new InvalidOrganizationDataException( + "logoUrl", + "invalid URL format" + ); + } + return this.update({ logoUrl }); + } + + public updateWebsite(website: string): OrganizationEntity { + if (website && !this.isValidUrl(website)) { + throw new InvalidOrganizationDataException( + "website", + "invalid URL format" + ); + } + return this.update({ website }); + } + + // Método privado para validar URLs + private isValidUrl(url: string): boolean { + try { + new URL(url); + return true; + } catch { + return false; + } + } } diff --git a/src/modules/organization/domain/interfaces/organization-repository.interface.ts b/src/modules/organization/domain/interfaces/organization-repository.interface.ts deleted file mode 100644 index b0151c4..0000000 --- a/src/modules/organization/domain/interfaces/organization-repository.interface.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Organization } from "../entities/organization.entity"; - -export interface IOrganizationRepository { - save(organization: Organization): Promise; - findById(id: string): Promise; - findByEmail(email: string): Promise; - findAll(options?: { - page?: number; - limit?: number; - search?: string; - }): Promise; - update(id: string, organization: Organization): Promise; - delete(id: string): Promise; -} diff --git a/src/modules/organization/infrastructure/index.ts b/src/modules/organization/infrastructure/index.ts new file mode 100644 index 0000000..7dde50d --- /dev/null +++ b/src/modules/organization/infrastructure/index.ts @@ -0,0 +1 @@ +export { OrganizationRepository } from "./repositories/organization.repository"; diff --git a/src/modules/organization/infrastructure/repositories/organization.repository.ts b/src/modules/organization/infrastructure/repositories/organization.repository.ts new file mode 100644 index 0000000..0e18297 --- /dev/null +++ b/src/modules/organization/infrastructure/repositories/organization.repository.ts @@ -0,0 +1,212 @@ +import { PrismaClient } from "@prisma/client"; +import { IOrganizationRepository } from "../../application/repository/organization.repository"; +import { OrganizationEntity } from "../../domain/entities/organization.entity"; + +export class OrganizationRepository implements IOrganizationRepository { + constructor(private readonly prisma: PrismaClient) {} + + async save(organization: OrganizationEntity): Promise { + const savedOrg = await this.prisma.organization.upsert({ + where: { id: organization.id }, + update: { + name: organization.name, + email: organization.email, + description: organization.description, + category: organization.category, + website: organization.website, + address: organization.address, + phone: organization.phone, + isVerified: organization.isVerified, + updatedAt: new Date(), + }, + create: { + id: organization.id, + name: organization.name, + email: organization.email, + description: organization.description, + category: organization.category, + website: organization.website, + address: organization.address, + phone: organization.phone, + isVerified: organization.isVerified, + createdAt: new Date(), + updatedAt: new Date(), + }, + }); + + return OrganizationEntity.create({ + name: savedOrg.name, + email: savedOrg.email, + description: savedOrg.description, + category: savedOrg.category, + website: savedOrg.website, + address: savedOrg.address, + phone: savedOrg.phone, + isVerified: savedOrg.isVerified, + }); + } + + async findById(id: string): Promise { + const org = await this.prisma.organization.findUnique({ + where: { id }, + }); + + if (!org) return null; + + return OrganizationEntity.create({ + name: org.name, + email: org.email, + description: org.description, + category: org.category, + website: org.website, + address: org.address, + phone: org.phone, + isVerified: org.isVerified, + }); + } + + async findByEmail(email: string): Promise { + const org = await this.prisma.organization.findUnique({ + where: { email }, + }); + + if (!org) return null; + + return OrganizationEntity.create({ + name: org.name, + email: org.email, + description: org.description, + category: org.category, + website: org.website, + address: org.address, + phone: org.phone, + isVerified: org.isVerified, + }); + } + + // Polimorfismo: Dos implementaciones de findAll + async findAll(options?: { + page?: number; + limit?: number; + search?: string; + }): Promise; + async findAll( + page?: number, + limit?: number, + search?: string + ): Promise; + async findAll( + pageOrOptions?: number | { page?: number; limit?: number; search?: string }, + limit?: number, + search?: string + ): Promise { + // Determinar qué versión del método se está llamando + if (typeof pageOrOptions === "object") { + // Llamada con objeto: findAll({ page: 1, limit: 10, search: "test" }) + const { + page = 1, + limit: limitParam = 10, + search: searchParam, + } = pageOrOptions; + return this._findAllInternal(page, limitParam, searchParam); + } else { + // Llamada con parámetros separados: findAll(1, 10, "test") + const page = pageOrOptions || 1; + const limitParam = limit || 10; + const searchParam = search; + return this._findAllInternal(page, limitParam, searchParam); + } + } + + // Método privado que contiene la lógica común + private async _findAllInternal( + page: number, + limit: number, + search?: string + ): Promise { + const skip = (page - 1) * limit; + + const where = search + ? { + OR: [ + { name: { contains: search, mode: "insensitive" } }, + { email: { contains: search, mode: "insensitive" } }, + { description: { contains: search, mode: "insensitive" } }, + { category: { contains: search, mode: "insensitive" } }, + ], + } + : {}; + + const orgs = await this.prisma.organization.findMany({ + where, + skip, + take: limit, + orderBy: { createdAt: "desc" }, + }); + + return orgs.map((org: (typeof orgs)[0]) => + OrganizationEntity.create({ + name: org.name, + email: org.email, + description: org.description, + category: org.category, + website: org.website, + address: org.address, + phone: org.phone, + isVerified: org.isVerified, + }) + ); + } + + async update( + id: string, + organization: OrganizationEntity + ): Promise { + const updatedOrg = await this.prisma.organization.update({ + where: { id }, + data: { + name: organization.name, + email: organization.email, + description: organization.description, + category: organization.category, + website: organization.website, + address: organization.address, + phone: organization.phone, + isVerified: organization.isVerified, + updatedAt: new Date(), + }, + }); + + return OrganizationEntity.create({ + name: updatedOrg.name, + email: updatedOrg.email, + description: updatedOrg.description, + category: updatedOrg.category, + website: updatedOrg.website, + address: updatedOrg.address, + phone: updatedOrg.phone, + isVerified: updatedOrg.isVerified, + }); + } + + async delete(id: string): Promise { + await this.prisma.organization.delete({ + where: { id }, + }); + } + + async count(search?: string): Promise { + const where = search + ? { + OR: [ + { name: { contains: search, mode: "insensitive" } }, + { email: { contains: search, mode: "insensitive" } }, + { description: { contains: search, mode: "insensitive" } }, + { category: { contains: search, mode: "insensitive" } }, + ], + } + : {}; + + return this.prisma.organization.count({ where }); + } +} diff --git a/src/modules/organization/presentation/controllers/OrganizationController.disabled b/src/modules/organization/presentation/controllers/OrganizationController.disabled deleted file mode 100644 index 2c8029d..0000000 --- a/src/modules/organization/presentation/controllers/OrganizationController.disabled +++ /dev/null @@ -1,86 +0,0 @@ -import { Request, Response } from "express"; -import { OrganizationService } from "../../../../services/OrganizationService"; -import { asyncHandler } from "../../../../utils/asyncHandler"; - -class OrganizationController { - private organizationService: OrganizationService; - - constructor() { - this.organizationService = new OrganizationService(); - } - - createOrganization = asyncHandler( - async (req: Request, res: Response): Promise => { - const { name, email, password, category, wallet } = req.body; - const organization = await this.organizationService.createOrganization( - name, - email, - password, - category, - wallet - ); - res.status(201).json(organization); - } - ); - - getOrganizationById = asyncHandler( - async (req: Request, res: Response): Promise => { - const { id } = req.params; - const organization = - await this.organizationService.getOrganizationById(id); - - if (!organization) { - res.status(404).json({ error: "Organization not found" }); - return; - } - - res.status(200).json(organization); - } - ); - - getOrganizationByEmail = asyncHandler( - async (req: Request, res: Response): Promise => { - const { email } = req.params; - const organization = - await this.organizationService.getOrganizationByEmail(email); - - if (!organization) { - res.status(404).json({ error: "Organization not found" }); - return; - } - - res.status(200).json(organization); - } - ); - - updateOrganization = asyncHandler( - async (req: Request, res: Response): Promise => { - const { id } = req.params; - const updateData = req.body; - - const organization = await this.organizationService.updateOrganization( - id, - updateData - ); - res.status(200).json(organization); - } - ); - - deleteOrganization = asyncHandler( - async (req: Request, res: Response): Promise => { - const { id } = req.params; - await this.organizationService.deleteOrganization(id); - res.status(204).send(); - } - ); - - getAllOrganizations = asyncHandler( - async (req: Request, res: Response): Promise => { - const organizations = - await this.organizationService.getAllOrganizations(); - res.status(200).json(organizations); - } - ); -} - -export default new OrganizationController(); diff --git a/src/modules/organization/presentation/controllers/OrganizationController.stub.ts b/src/modules/organization/presentation/controllers/OrganizationController.stub.ts deleted file mode 100644 index 7c85c07..0000000 --- a/src/modules/organization/presentation/controllers/OrganizationController.stub.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Request, Response } from "express"; - -/** - * Stub controller for Organization functionality - * This replaces the original controller that referenced deleted services - * TODO: Implement proper organization controller using new modular architecture - */ -class OrganizationController { - async createOrganization(req: Request, res: Response) { - res.status(501).json({ - message: "Organization service temporarily disabled during migration", - error: "Service migration in progress" - }); - } - - async getAllOrganizations(req: Request, res: Response) { - res.status(501).json({ - message: "Organization service temporarily disabled during migration", - error: "Service migration in progress" - }); - } - - async getOrganizationById(req: Request, res: Response) { - res.status(501).json({ - message: "Organization service temporarily disabled during migration", - error: "Service migration in progress" - }); - } - - async getOrganizationByEmail(req: Request, res: Response) { - res.status(501).json({ - message: "Organization service temporarily disabled during migration", - error: "Service migration in progress" - }); - } - - async updateOrganization(req: Request, res: Response) { - res.status(501).json({ - message: "Organization service temporarily disabled during migration", - error: "Service migration in progress" - }); - } - - async deleteOrganization(req: Request, res: Response) { - res.status(501).json({ - message: "Organization service temporarily disabled during migration", - error: "Service migration in progress" - }); - } -} - -export default new OrganizationController(); \ No newline at end of file diff --git a/src/modules/organization/presentation/controllers/organization.controller.ts b/src/modules/organization/presentation/controllers/organization.controller.ts index e306815..6910bf6 100644 --- a/src/modules/organization/presentation/controllers/organization.controller.ts +++ b/src/modules/organization/presentation/controllers/organization.controller.ts @@ -1,17 +1,14 @@ import { Request, Response } from "express"; import { asyncHandler } from "../../../shared/infrastructure/utils/async-handler"; -import { CreateOrganizationUseCase } from "../../application/use-cases/create-organization.usecase"; -import { GetOrganizationByIdUseCase } from "../../application/use-cases/get-organization-by-id.usecase"; -import { UpdateOrganizationUseCase } from "../../application/use-cases/update-organization.usecase"; -import { DeleteOrganizationUseCase } from "../../application/use-cases/delete-organization.usecase"; -import { GetAllOrganizationsUseCase } from "../../application/use-cases/get-all-organizations.usecase"; -import { CreateOrganizationDto } from "../dto/create-organization.dto"; -import { UpdateOrganizationDto } from "../dto/update-organization.dto"; +import { CreateOrganizationUseCase } from "../../application/use-cases/create-organization.use-case"; +import { GetOrganizationByIdUseCase } from "../../application/use-cases/get-organization-by-id.use-case"; +import { UpdateOrganizationUseCase } from "../../application/use-cases/update-organization.use-case"; +import { DeleteOrganizationUseCase } from "../../application/use-cases/delete-organization.use-case"; +import { GetAllOrganizationsUseCase } from "../../application/use-cases/get-all-organizations.use-case"; +import { CreateOrganizationDto, UpdateOrganizationDto } from "../dto"; + import { OrganizationNotFoundException } from "../../domain/exceptions/organization-not-found.exception"; -import { - UuidParamsDto, - PaginationQueryDto, -} from "../../../shared/dto/base.dto"; +import { PaginationQueryDto } from "@/shared/dto/base.dto"; export class OrganizationController { constructor( @@ -23,12 +20,11 @@ export class OrganizationController { ) {} createOrganization = asyncHandler( - async ( - req: Request, - res: Response - ): Promise => { + async (req: Request, res: Response): Promise => { + const organizationInformation: CreateOrganizationDto = req.body; + const organization = await this.createOrganizationUseCase.execute( - req.body + organizationInformation ); res.status(201).json({ @@ -40,7 +36,7 @@ export class OrganizationController { ); getOrganizationById = asyncHandler( - async (req: Request, res: Response): Promise => { + async (req: Request, res: Response): Promise => { const { id } = req.params; try { @@ -64,16 +60,14 @@ export class OrganizationController { ); updateOrganization = asyncHandler( - async ( - req: Request, - res: Response - ): Promise => { + async (req: Request, res: Response): Promise => { const { id } = req.params; + const { ...organizationInformation }: UpdateOrganizationDto = req.body; try { const organization = await this.updateOrganizationUseCase.execute( id, - req.body + organizationInformation ); res.status(200).json({ @@ -95,7 +89,7 @@ export class OrganizationController { ); deleteOrganization = asyncHandler( - async (req: Request, res: Response): Promise => { + async (req: Request, res: Response): Promise => { const { id } = req.params; try { @@ -116,11 +110,8 @@ export class OrganizationController { ); getAllOrganizations = asyncHandler( - async ( - req: Request, - res: Response - ): Promise => { - const { page, limit, search } = req.query; + async (req: Request, res: Response): Promise => { + const { page, limit, search }: PaginationQueryDto = req.query; const organizations = await this.getAllOrganizationsUseCase.execute({ page: page || 1, diff --git a/src/modules/organization/presentation/dto/index.ts b/src/modules/organization/presentation/dto/index.ts new file mode 100644 index 0000000..34a8d69 --- /dev/null +++ b/src/modules/organization/presentation/dto/index.ts @@ -0,0 +1,2 @@ +export { CreateOrganizationDto } from "./create-organization.dto"; +export { UpdateOrganizationDto } from "./update-organization.dto"; diff --git a/src/modules/organization/presentation/routes.ts b/src/modules/organization/presentation/routes.ts new file mode 100644 index 0000000..30ac62e --- /dev/null +++ b/src/modules/organization/presentation/routes.ts @@ -0,0 +1,86 @@ +import { Router } from "express"; + +//Middlewares +import auth from "../../../middleware/authMiddleware"; + +//Validate middleware +import { + validateDto, + validateParamsDto, + validateQueryDto, +} from "../../../shared/middleware/validation.middleware"; + +// Dto base +import { + UuidParamsDto, + PaginationQueryDto, +} from "../../../shared/dto/base.dto"; + +// Dto +import { CreateOrganizationDto, UpdateOrganizationDto } from "./dto"; + +//Controller +import { OrganizationController } from "./controllers/organization.controller"; + +// Use Cases +import { + CreateOrganizationUseCase, + DeleteOrganizationUseCase, + GetAllOrganizationsUseCase, + GetOrganizationByIdUseCase, + UpdateOrganizationUseCase, +} from "../application/use-cases/index"; + +// Repository Impl +import { OrganizationRepository } from "../infrastructure"; + +// Prisma instance +import prisma from "../../../config/prisma"; + +const router = Router(); + +const repository = new OrganizationRepository(prisma); +const controller = new OrganizationController( + new CreateOrganizationUseCase(repository), + new GetOrganizationByIdUseCase(repository), + new UpdateOrganizationUseCase(repository), + new DeleteOrganizationUseCase(repository), + new GetAllOrganizationsUseCase(repository) +); + +// Public routes +router.post( + "/", + validateDto(CreateOrganizationDto), + controller.createOrganization +); + +router.get( + "/", + validateQueryDto(PaginationQueryDto), + controller.getAllOrganizations +); + +router.get( + "/:id", + validateParamsDto(UuidParamsDto), + controller.getOrganizationById +); + +// Protected routes (require authentication) +router.put( + "/:id", + auth.authMiddleware, + validateParamsDto(UuidParamsDto), + validateDto(UpdateOrganizationDto), + controller.updateOrganization +); + +router.delete( + "/:id", + auth.authMiddleware, + validateParamsDto(UuidParamsDto), + controller.deleteOrganization +); + +export default router; diff --git a/src/modules/shared/infrastructure/utils/async-handler.ts b/src/modules/shared/infrastructure/utils/async-handler.ts index 16e3e68..859f069 100644 --- a/src/modules/shared/infrastructure/utils/async-handler.ts +++ b/src/modules/shared/infrastructure/utils/async-handler.ts @@ -1,13 +1,7 @@ -import { Request, Response, NextFunction } from "express"; +import { NextFunction, Request, Response } from "express"; -type AsyncFunction = ( - req: Request, - res: Response, - next: NextFunction -) => Promise; - -export const asyncHandler = (fn: AsyncFunction) => { - return (req: Request, res: Response, next: NextFunction) => { +export const asyncHandler = + (fn: (req: Request, res: Response, next: NextFunction) => Promise) => + (req: Request, res: Response, next: NextFunction) => { Promise.resolve(fn(req, res, next)).catch(next); }; -}; diff --git a/src/routes/organization.routes.ts b/src/routes/organization.routes.ts deleted file mode 100644 index b58e338..0000000 --- a/src/routes/organization.routes.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Router } from "express"; -import { - validateDto, - validateParamsDto, - validateQueryDto, -} from "../../shared/middleware/validation.middleware"; -import { CreateOrganizationDto } from "../../modules/organization/presentation/dto/create-organization.dto"; -import { UpdateOrganizationDto } from "../../modules/organization/presentation/dto/update-organization.dto"; -import { UuidParamsDto, PaginationQueryDto } from "../../shared/dto/base.dto"; -import auth from "../../middleware/authMiddleware"; - -const router = Router(); - -// Note: This is an example of how to properly integrate validation middleware -// The controller would need to be properly instantiated with dependencies - -// POST /organizations - Create organization -router.post( - "/", - validateDto(CreateOrganizationDto) - // organizationController.createOrganization -); - -// GET /organizations - Get all organizations with pagination -router.get( - "/", - validateQueryDto(PaginationQueryDto) - // organizationController.getAllOrganizations -); - -// GET /organizations/:id - Get organization by ID -router.get( - "/:id", - validateParamsDto(UuidParamsDto) - // organizationController.getOrganizationById -); - -// PUT /organizations/:id - Update organization (protected) -router.put( - "/:id", - auth.authMiddleware, - validateParamsDto(UuidParamsDto), - validateDto(UpdateOrganizationDto) - // organizationController.updateOrganization -); - -// DELETE /organizations/:id - Delete organization (protected) -router.delete( - "/:id", - auth.authMiddleware, - validateParamsDto(UuidParamsDto) - // organizationController.deleteOrganization -); - -export default router; diff --git a/src/shared/dto/base.dto.ts b/src/shared/dto/base.dto.ts index 1f71a27..76ffee6 100644 --- a/src/shared/dto/base.dto.ts +++ b/src/shared/dto/base.dto.ts @@ -1,9 +1,8 @@ import { IsUUID, IsOptional, IsInt, Min, IsString } from "class-validator"; import { Transform } from "class-transformer"; - export class UuidParamsDto { @IsUUID(4, { message: "ID must be a valid UUID" }) - id: string; + id!: string; } export class PaginationQueryDto { @@ -29,7 +28,7 @@ export class BaseResponseDto { message?: string; } -export class ErrorResponseDto extends BaseResponseDto { +export class ErrorResponseDto { success: false; error: string; details?: Array<{ diff --git a/src/shared/infrastructure/container.ts b/src/shared/infrastructure/container.ts index f31aa07..b620357 100644 --- a/src/shared/infrastructure/container.ts +++ b/src/shared/infrastructure/container.ts @@ -1,6 +1,8 @@ import { CertificateService } from "../../modules/certificate/application/services/CertificateService"; import { ICertificateService } from "../../modules/certificate/domain/interfaces/ICertificateService"; +import { prisma } from "./prisma"; export const container = { certificateService: new CertificateService() as ICertificateService, + prisma, };