diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 63b7647..2ad8548 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -26,7 +26,7 @@ import { AuditModule } from './application/audit/audit.module'; import { NotificationsModule } from './application/notifications/notifications.module'; import { WebhooksModule } from './application/webhooks/webhooks.module'; import { GDPRModule } from './application/gdpr/gdpr.module'; -import { CsvBookingsModule } from './application/csv-bookings.module'; +import { CsvBookingsModule } from './application/csv-bookings/csv-bookings.module'; import { AdminModule } from './application/admin/admin.module'; import { BlogModule } from './application/blog/blog.module'; import { LogsModule } from './application/logs/logs.module'; diff --git a/apps/backend/src/application/admin/admin.module.ts b/apps/backend/src/application/admin/admin.module.ts index ba4f5e1..e834f3b 100644 --- a/apps/backend/src/application/admin/admin.module.ts +++ b/apps/backend/src/application/admin/admin.module.ts @@ -24,7 +24,7 @@ import { SIRET_VERIFICATION_PORT } from '@domain/ports/out/siret-verification.po import { PappersSiretAdapter } from '@infrastructure/external/pappers-siret.adapter'; // CSV Booking Service -import { CsvBookingsModule } from '../csv-bookings.module'; +import { CsvBookingsModule } from '../csv-bookings/csv-bookings.module'; // Email import { EmailModule } from '@infrastructure/email/email.module'; diff --git a/apps/backend/src/application/controllers/index.ts b/apps/backend/src/application/controllers/index.ts index 70e2402..31be0d7 100644 --- a/apps/backend/src/application/controllers/index.ts +++ b/apps/backend/src/application/controllers/index.ts @@ -1,2 +1,16 @@ export * from './rates.controller'; export * from './bookings.controller'; +export * from './auth.controller'; +export * from './users.controller'; +export * from './organizations.controller'; +export * from './ports.controller'; +export * from './notifications.controller'; +export * from './webhooks.controller'; +export * from './audit.controller'; +export * from './subscriptions.controller'; +export * from './invitations.controller'; +export * from './gdpr.controller'; +export * from './health.controller'; +export * from './blog.controller'; +export * from './csv-bookings.controller'; +export * from './csv-booking-actions.controller'; diff --git a/apps/backend/src/application/csv-bookings.module.ts b/apps/backend/src/application/csv-bookings.module.ts deleted file mode 100644 index 6330924..0000000 --- a/apps/backend/src/application/csv-bookings.module.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { ConfigModule } from '@nestjs/config'; -import { CsvBookingsController } from './controllers/csv-bookings.controller'; -import { CsvBookingActionsController } from './controllers/csv-booking-actions.controller'; -import { CsvBookingService } from './services/csv-booking.service'; -import { CsvBookingOrmEntity } from '../infrastructure/persistence/typeorm/entities/csv-booking.orm-entity'; -import { TypeOrmCsvBookingRepository } from '../infrastructure/persistence/typeorm/repositories/csv-booking.repository'; -import { TypeOrmShipmentCounterRepository } from '../infrastructure/persistence/typeorm/repositories/shipment-counter.repository'; -import { SHIPMENT_COUNTER_PORT } from '@domain/ports/out/shipment-counter.port'; -import { ORGANIZATION_REPOSITORY } from '@domain/ports/out/organization.repository'; -import { OrganizationOrmEntity } from '../infrastructure/persistence/typeorm/entities/organization.orm-entity'; -import { TypeOrmOrganizationRepository } from '../infrastructure/persistence/typeorm/repositories/typeorm-organization.repository'; -import { USER_REPOSITORY } from '@domain/ports/out/user.repository'; -import { UserOrmEntity } from '../infrastructure/persistence/typeorm/entities/user.orm-entity'; -import { TypeOrmUserRepository } from '../infrastructure/persistence/typeorm/repositories/typeorm-user.repository'; -import { NotificationsModule } from './notifications/notifications.module'; -import { EmailModule } from '../infrastructure/email/email.module'; -import { StorageModule } from '../infrastructure/storage/storage.module'; -import { SubscriptionsModule } from './subscriptions/subscriptions.module'; -import { StripeModule } from '../infrastructure/stripe/stripe.module'; - -/** - * CSV Bookings Module - * - * Handles CSV-based booking workflow with carrier email confirmations - */ -@Module({ - imports: [ - TypeOrmModule.forFeature([CsvBookingOrmEntity, OrganizationOrmEntity, UserOrmEntity]), - ConfigModule, - NotificationsModule, - EmailModule, - StorageModule, - SubscriptionsModule, - StripeModule, - ], - controllers: [CsvBookingsController, CsvBookingActionsController], - providers: [ - CsvBookingService, - TypeOrmCsvBookingRepository, - { - provide: SHIPMENT_COUNTER_PORT, - useClass: TypeOrmShipmentCounterRepository, - }, - { - provide: ORGANIZATION_REPOSITORY, - useClass: TypeOrmOrganizationRepository, - }, - { - provide: USER_REPOSITORY, - useClass: TypeOrmUserRepository, - }, - ], - exports: [CsvBookingService, TypeOrmCsvBookingRepository], -}) -export class CsvBookingsModule {} diff --git a/apps/backend/src/application/csv-bookings/csv-bookings.module.ts b/apps/backend/src/application/csv-bookings/csv-bookings.module.ts new file mode 100644 index 0000000..6f7a20a --- /dev/null +++ b/apps/backend/src/application/csv-bookings/csv-bookings.module.ts @@ -0,0 +1,57 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ConfigModule } from '@nestjs/config'; +import { CsvBookingsController } from '../controllers/csv-bookings.controller'; +import { CsvBookingActionsController } from '../controllers/csv-booking-actions.controller'; +import { CsvBookingService } from '../services/csv-booking.service'; +import { CsvBookingOrmEntity } from '../../infrastructure/persistence/typeorm/entities/csv-booking.orm-entity'; +import { TypeOrmCsvBookingRepository } from '../../infrastructure/persistence/typeorm/repositories/csv-booking.repository'; +import { TypeOrmShipmentCounterRepository } from '../../infrastructure/persistence/typeorm/repositories/shipment-counter.repository'; +import { SHIPMENT_COUNTER_PORT } from '@domain/ports/out/shipment-counter.port'; +import { ORGANIZATION_REPOSITORY } from '@domain/ports/out/organization.repository'; +import { OrganizationOrmEntity } from '../../infrastructure/persistence/typeorm/entities/organization.orm-entity'; +import { TypeOrmOrganizationRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-organization.repository'; +import { USER_REPOSITORY } from '@domain/ports/out/user.repository'; +import { UserOrmEntity } from '../../infrastructure/persistence/typeorm/entities/user.orm-entity'; +import { TypeOrmUserRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-user.repository'; +import { NotificationsModule } from '../notifications/notifications.module'; +import { EmailModule } from '../../infrastructure/email/email.module'; +import { StorageModule } from '../../infrastructure/storage/storage.module'; +import { SubscriptionsModule } from '../subscriptions/subscriptions.module'; +import { StripeModule } from '../../infrastructure/stripe/stripe.module'; + +/** + * CSV Bookings Module + * + * Handles CSV-based booking workflow with carrier email confirmations + */ +@Module({ + imports: [ + TypeOrmModule.forFeature([CsvBookingOrmEntity, OrganizationOrmEntity, UserOrmEntity]), + ConfigModule, + NotificationsModule, + EmailModule, + StorageModule, + SubscriptionsModule, + StripeModule, + ], + controllers: [CsvBookingsController, CsvBookingActionsController], + providers: [ + CsvBookingService, + TypeOrmCsvBookingRepository, + { + provide: SHIPMENT_COUNTER_PORT, + useClass: TypeOrmShipmentCounterRepository, + }, + { + provide: ORGANIZATION_REPOSITORY, + useClass: TypeOrmOrganizationRepository, + }, + { + provide: USER_REPOSITORY, + useClass: TypeOrmUserRepository, + }, + ], + exports: [CsvBookingService, TypeOrmCsvBookingRepository], +}) +export class CsvBookingsModule {} diff --git a/apps/backend/src/application/dashboard/dashboard.module.ts b/apps/backend/src/application/dashboard/dashboard.module.ts index b483b11..4b3edc9 100644 --- a/apps/backend/src/application/dashboard/dashboard.module.ts +++ b/apps/backend/src/application/dashboard/dashboard.module.ts @@ -7,7 +7,7 @@ import { DashboardController } from './dashboard.controller'; import { AnalyticsService } from '../services/analytics.service'; import { BookingsModule } from '../bookings/bookings.module'; import { RatesModule } from '../rates/rates.module'; -import { CsvBookingsModule } from '../csv-bookings.module'; +import { CsvBookingsModule } from '../csv-bookings/csv-bookings.module'; import { SubscriptionsModule } from '../subscriptions/subscriptions.module'; import { FeatureFlagGuard } from '../guards/feature-flag.guard'; diff --git a/apps/backend/src/application/decorators/index.ts b/apps/backend/src/application/decorators/index.ts index 76ef1b6..5720b2b 100644 --- a/apps/backend/src/application/decorators/index.ts +++ b/apps/backend/src/application/decorators/index.ts @@ -1,3 +1,4 @@ export * from './current-user.decorator'; export * from './public.decorator'; export * from './roles.decorator'; +export * from './requires-feature.decorator'; diff --git a/apps/backend/src/application/guards/index.ts b/apps/backend/src/application/guards/index.ts index 374d66f..b6998e3 100644 --- a/apps/backend/src/application/guards/index.ts +++ b/apps/backend/src/application/guards/index.ts @@ -1,3 +1,5 @@ export * from './jwt-auth.guard'; export * from './roles.guard'; export * from './api-key-or-jwt.guard'; +export * from './feature-flag.guard'; +export * from './throttle.guard'; diff --git a/apps/backend/src/application/mappers/index.ts b/apps/backend/src/application/mappers/index.ts index 930a103..7628cfb 100644 --- a/apps/backend/src/application/mappers/index.ts +++ b/apps/backend/src/application/mappers/index.ts @@ -1,3 +1,6 @@ export * from './rate-quote.mapper'; export * from './booking.mapper'; export * from './port.mapper'; +export * from './user.mapper'; +export * from './organization.mapper'; +export * from './csv-rate.mapper'; diff --git a/apps/backend/src/application/rates/rates.module.ts b/apps/backend/src/application/rates/rates.module.ts index 583fa87..af47ef8 100644 --- a/apps/backend/src/application/rates/rates.module.ts +++ b/apps/backend/src/application/rates/rates.module.ts @@ -45,12 +45,16 @@ import { CarrierOrmEntity } from '../../infrastructure/persistence/typeorm/entit }, { provide: RateSearchService, - useFactory: (cache: any, rateQuoteRepo: any, portRepo: any, carrierRepo: any) => { - // For now, create service with empty connectors array - // TODO: Inject actual carrier connectors - return new RateSearchService([], cache, rateQuoteRepo, portRepo, carrierRepo); + useFactory: ( + connectors: any[], + cache: any, + rateQuoteRepo: any, + portRepo: any, + carrierRepo: any, + ) => { + return new RateSearchService(connectors, cache, rateQuoteRepo, portRepo, carrierRepo); }, - inject: [CACHE_PORT, RATE_QUOTE_REPOSITORY, PORT_REPOSITORY, CARRIER_REPOSITORY], + inject: ['CarrierConnectors', CACHE_PORT, RATE_QUOTE_REPOSITORY, PORT_REPOSITORY, CARRIER_REPOSITORY], }, ], exports: [RATE_QUOTE_REPOSITORY, RateSearchService], diff --git a/apps/backend/src/application/services/carrier-auth.service.ts b/apps/backend/src/application/services/carrier-auth.service.ts deleted file mode 100644 index a126133..0000000 --- a/apps/backend/src/application/services/carrier-auth.service.ts +++ /dev/null @@ -1,318 +0,0 @@ -/** - * Carrier Auth Service - * - * Handles carrier authentication and automatic account creation - */ - -import { Injectable, Logger, UnauthorizedException, Inject } from '@nestjs/common'; -import { JwtService } from '@nestjs/jwt'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { CarrierProfileRepository } from '@infrastructure/persistence/typeorm/repositories/carrier-profile.repository'; -import { UserOrmEntity } from '@infrastructure/persistence/typeorm/entities/user.orm-entity'; -import { OrganizationOrmEntity } from '@infrastructure/persistence/typeorm/entities/organization.orm-entity'; -import { EmailPort, EMAIL_PORT } from '@domain/ports/out/email.port'; -import * as argon2 from 'argon2'; -import { randomBytes } from 'crypto'; -import { v4 as uuidv4 } from 'uuid'; - -@Injectable() -export class CarrierAuthService { - private readonly logger = new Logger(CarrierAuthService.name); - - constructor( - private readonly carrierProfileRepository: CarrierProfileRepository, - @InjectRepository(UserOrmEntity) - private readonly userRepository: Repository, - @InjectRepository(OrganizationOrmEntity) - private readonly organizationRepository: Repository, - private readonly jwtService: JwtService, - @Inject(EMAIL_PORT) - private readonly emailAdapter: EmailPort - ) {} - - /** - * Create carrier account automatically when clicking accept/reject link - */ - async createCarrierAccountIfNotExists( - carrierEmail: string, - carrierName: string - ): Promise<{ - carrierId: string; - userId: string; - isNewAccount: boolean; - temporaryPassword?: string; - }> { - this.logger.log(`Checking/creating carrier account for: ${carrierEmail}`); - - // Check if carrier already exists - const existingCarrier = await this.carrierProfileRepository.findByEmail(carrierEmail); - - if (existingCarrier) { - this.logger.log(`Carrier already exists: ${carrierEmail}`); - return { - carrierId: existingCarrier.id, - userId: existingCarrier.userId, - isNewAccount: false, - }; - } - - // Create new organization for the carrier - const organizationId = uuidv4(); // Generate UUID for organization - const organization = this.organizationRepository.create({ - id: organizationId, // Provide explicit ID since @PrimaryColumn requires it - name: carrierName, - type: 'CARRIER', - isCarrier: true, - carrierType: 'LCL', // Default - addressStreet: 'TBD', - addressCity: 'TBD', - addressPostalCode: 'TBD', - addressCountry: 'FR', // Default to France - isActive: true, - }); - - const savedOrganization = await this.organizationRepository.save(organization); - this.logger.log(`Created organization: ${savedOrganization.id}`); - - // Generate temporary password - const temporaryPassword = this.generateTemporaryPassword(); - const hashedPassword = await argon2.hash(temporaryPassword); - - // Create user account - const nameParts = carrierName.split(' '); - const user = this.userRepository.create({ - id: uuidv4(), - email: carrierEmail.toLowerCase(), - passwordHash: hashedPassword, - firstName: nameParts[0] || 'Carrier', - lastName: nameParts.slice(1).join(' ') || 'Account', - role: 'CARRIER', // New role for carriers - organizationId: savedOrganization.id, - isActive: true, - isEmailVerified: true, // Auto-verified since created via email - }); - - const savedUser = await this.userRepository.save(user); - this.logger.log(`Created user: ${savedUser.id}`); - - // Create carrier profile - const carrierProfile = await this.carrierProfileRepository.create({ - userId: savedUser.id, - organizationId: savedOrganization.id, - companyName: carrierName, - notificationEmail: carrierEmail, - preferredCurrency: 'USD', - isActive: true, - isVerified: false, // Will be verified later - }); - - this.logger.log(`Created carrier profile: ${carrierProfile.id}`); - - // Send welcome email with credentials and WAIT for confirmation - try { - await this.emailAdapter.sendCarrierAccountCreated( - carrierEmail, - carrierName, - temporaryPassword - ); - this.logger.log(`Account creation email sent to ${carrierEmail}`); - } catch (error: any) { - this.logger.error(`Failed to send account creation email: ${error?.message}`, error?.stack); - // Continue even if email fails - account is already created - } - - return { - carrierId: carrierProfile.id, - userId: savedUser.id, - isNewAccount: true, - temporaryPassword, - }; - } - - /** - * Generate auto-login JWT token for carrier - */ - async generateAutoLoginToken(userId: string, carrierId: string): Promise { - this.logger.log(`Generating auto-login token for carrier: ${carrierId}`); - - const payload = { - sub: userId, - carrierId, - type: 'carrier', - autoLogin: true, - }; - - const token = this.jwtService.sign(payload, { expiresIn: '1h' }); - this.logger.log(`Auto-login token generated for carrier: ${carrierId}`); - - return token; - } - - /** - * Standard login for carriers - */ - async login( - email: string, - password: string - ): Promise<{ - accessToken: string; - refreshToken: string; - carrier: { - id: string; - companyName: string; - email: string; - }; - }> { - this.logger.log(`Carrier login attempt: ${email}`); - - const carrier = await this.carrierProfileRepository.findByEmail(email); - - if (!carrier || !carrier.user) { - this.logger.warn(`Login failed: Carrier not found for email ${email}`); - throw new UnauthorizedException('Invalid credentials'); - } - - // Verify password - const isPasswordValid = await argon2.verify(carrier.user.passwordHash, password); - - if (!isPasswordValid) { - this.logger.warn(`Login failed: Invalid password for ${email}`); - throw new UnauthorizedException('Invalid credentials'); - } - - // Check if carrier is active - if (!carrier.isActive) { - this.logger.warn(`Login failed: Carrier account is inactive ${email}`); - throw new UnauthorizedException('Account is inactive'); - } - - // Update last login - await this.carrierProfileRepository.updateLastLogin(carrier.id); - - // Generate JWT tokens - const payload = { - sub: carrier.userId, - email: carrier.user.email, - carrierId: carrier.id, - organizationId: carrier.organizationId, - role: 'CARRIER', - }; - - const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' }); - const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' }); - - this.logger.log(`Login successful for carrier: ${carrier.id}`); - - return { - accessToken, - refreshToken, - carrier: { - id: carrier.id, - companyName: carrier.companyName, - email: carrier.user.email, - }, - }; - } - - /** - * Verify auto-login token - */ - async verifyAutoLoginToken(token: string): Promise<{ - userId: string; - carrierId: string; - }> { - try { - const payload = this.jwtService.verify(token); - - if (!payload.autoLogin || payload.type !== 'carrier') { - throw new UnauthorizedException('Invalid auto-login token'); - } - - return { - userId: payload.sub, - carrierId: payload.carrierId, - }; - } catch (error: any) { - this.logger.error(`Auto-login token verification failed: ${error?.message}`); - throw new UnauthorizedException('Invalid or expired token'); - } - } - - /** - * Change carrier password - */ - async changePassword(carrierId: string, oldPassword: string, newPassword: string): Promise { - this.logger.log(`Password change request for carrier: ${carrierId}`); - - const carrier = await this.carrierProfileRepository.findById(carrierId); - - if (!carrier || !carrier.user) { - throw new UnauthorizedException('Carrier not found'); - } - - // Verify old password - const isOldPasswordValid = await argon2.verify(carrier.user.passwordHash, oldPassword); - - if (!isOldPasswordValid) { - this.logger.warn(`Password change failed: Invalid old password for carrier ${carrierId}`); - throw new UnauthorizedException('Invalid old password'); - } - - // Hash new password - const hashedNewPassword = await argon2.hash(newPassword); - - // Update password - carrier.user.passwordHash = hashedNewPassword; - await this.userRepository.save(carrier.user); - - this.logger.log(`Password changed successfully for carrier: ${carrierId}`); - } - - /** - * Request password reset (sends temporary password via email) - */ - async requestPasswordReset(email: string): Promise<{ temporaryPassword: string }> { - this.logger.log(`Password reset request for: ${email}`); - - const carrier = await this.carrierProfileRepository.findByEmail(email); - - if (!carrier || !carrier.user) { - // Don't reveal if email exists or not for security - this.logger.warn(`Password reset requested for non-existent carrier: ${email}`); - throw new UnauthorizedException('If this email exists, a password reset will be sent'); - } - - // Generate temporary password - const temporaryPassword = this.generateTemporaryPassword(); - const hashedPassword = await argon2.hash(temporaryPassword); - - // Update password - carrier.user.passwordHash = hashedPassword; - await this.userRepository.save(carrier.user); - - this.logger.log(`Temporary password generated for carrier: ${carrier.id}`); - - // Send password reset email and WAIT for confirmation - try { - await this.emailAdapter.sendCarrierPasswordReset( - email, - carrier.companyName, - temporaryPassword - ); - this.logger.log(`Password reset email sent to ${email}`); - } catch (error: any) { - this.logger.error(`Failed to send password reset email: ${error?.message}`, error?.stack); - // Continue even if email fails - password is already reset - } - - return { temporaryPassword }; - } - - /** - * Generate a secure temporary password - */ - private generateTemporaryPassword(): string { - return randomBytes(16).toString('hex').slice(0, 12); - } -} diff --git a/apps/backend/src/domain/ports/in/index.ts b/apps/backend/src/domain/ports/in/index.ts index f41feef..32767c6 100644 --- a/apps/backend/src/domain/ports/in/index.ts +++ b/apps/backend/src/domain/ports/in/index.ts @@ -7,3 +7,4 @@ export * from './search-rates.port'; export * from './get-ports.port'; export * from './validate-availability.port'; +export * from './search-csv-rates.port'; diff --git a/apps/backend/src/domain/ports/out/index.ts b/apps/backend/src/domain/ports/out/index.ts index 9f47d85..ebc7d2f 100644 --- a/apps/backend/src/domain/ports/out/index.ts +++ b/apps/backend/src/domain/ports/out/index.ts @@ -15,6 +15,11 @@ export * from './notification.repository'; export * from './audit-log.repository'; export * from './webhook.repository'; export * from './csv-booking.repository'; +export * from './api-key.repository'; +export * from './blog-post.repository'; +export * from './invitation-token.repository'; +export * from './subscription.repository'; +export * from './license.repository'; // Infrastructure Ports export * from './cache.port'; @@ -23,6 +28,6 @@ export * from './pdf.port'; export * from './storage.port'; export * from './carrier-connector.port'; export * from './csv-rate-loader.port'; -export * from './subscription.repository'; -export * from './license.repository'; +export * from './shipment-counter.port'; +export * from './siret-verification.port'; export * from './stripe.port'; diff --git a/apps/backend/src/domain/services/index.ts b/apps/backend/src/domain/services/index.ts index 1d514e6..d2ff59c 100644 --- a/apps/backend/src/domain/services/index.ts +++ b/apps/backend/src/domain/services/index.ts @@ -8,3 +8,6 @@ export * from './rate-search.service'; export * from './port-search.service'; export * from './availability-validation.service'; export * from './booking.service'; +export * from './csv-rate-search.service'; +export * from './csv-rate-price-calculator.service'; +export * from './rate-offer-generator.service'; diff --git a/apps/backend/src/domain/value-objects/index.ts b/apps/backend/src/domain/value-objects/index.ts index 2ad876c..4a26880 100644 --- a/apps/backend/src/domain/value-objects/index.ts +++ b/apps/backend/src/domain/value-objects/index.ts @@ -15,3 +15,6 @@ export * from './subscription-plan.vo'; export * from './subscription-status.vo'; export * from './license-status.vo'; export * from './locale.vo'; +export * from './surcharge.vo'; +export * from './volume.vo'; +export * from './plan-feature.vo';