fix(api,ci): remove type-only imports for DI and isolate CI ports from dev

- Remove `type` keyword from NestJS injectable class imports across all
  modules to fix runtime DI resolution (330+ handler/listener files)
- Offset CI docker-compose ports (5433/6380/8109/9002) to avoid
  conflicts with running dev containers
- Update .env.test, playwright.config.ts, and e2e workflow to use
  isolated CI ports with configurable overrides
- Fix prisma/seed.ts to use deterministic IDs for Prisma 7 upsert
  compatibility (phoneHash replaced phone as unique index)
- Add dedicated Docker bridge network for CI service containers

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ho Ngoc Hai
2026-04-13 01:40:14 +07:00
parent 1617921993
commit 25420720e7
345 changed files with 3266 additions and 924 deletions

View File

@@ -1,4 +1,4 @@
import { type NotificationChannel } from '../../../domain/value-objects/notification-channel.vo';
import { NotificationChannel } from '../../../domain/value-objects/notification-channel.vo';
export class SendNotificationCommand {
constructor(

View File

@@ -1,18 +1,18 @@
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { DomainException, type EventBusService, type LoggerService } from '@modules/shared';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { DomainException, EventBusService, LoggerService } from '@modules/shared';
import { NotificationSentEvent } from '../../../domain/events/notification-sent.event';
import {
NOTIFICATION_PREFERENCE_REPOSITORY,
type INotificationPreferenceRepository,
INotificationPreferenceRepository,
} from '../../../domain/repositories/notification-preference.repository';
import {
NOTIFICATION_REPOSITORY,
type INotificationRepository,
INotificationRepository,
} from '../../../domain/repositories/notification.repository';
import { type EmailService } from '../../../infrastructure/services/email.service';
import { type FcmService } from '../../../infrastructure/services/fcm.service';
import { type TemplateService } from '../../../infrastructure/services/template.service';
import { EmailService } from '../../../infrastructure/services/email.service';
import { FcmService } from '../../../infrastructure/services/fcm.service';
import { TemplateService } from '../../../infrastructure/services/template.service';
import { SendNotificationCommand } from './send-notification.command';
@CommandHandler(SendNotificationCommand)

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type CommandBus } from '@nestjs/cqrs';
import { CommandBus } from '@nestjs/cqrs';
import { OnEvent } from '@nestjs/event-emitter';
import { type AgentVerifiedEvent } from '@modules/auth';
import { type LoggerService, type PrismaService } from '@modules/shared';
import { AgentVerifiedEvent } from '@modules/auth';
import { LoggerService, PrismaService } from '@modules/shared';
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
@Injectable()

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { type CommandBus } from '@nestjs/cqrs';
import { CommandBus } from '@nestjs/cqrs';
import { OnEvent } from '@nestjs/event-emitter';
import { type LoggerService, type PrismaService } from '@modules/shared';
import { LoggerService, PrismaService } from '@modules/shared';
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
export interface InquiryReceivedEvent {

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type CommandBus } from '@nestjs/cqrs';
import { CommandBus } from '@nestjs/cqrs';
import { OnEvent } from '@nestjs/event-emitter';
import { type ListingApprovedEvent } from '@modules/admin';
import { type LoggerService, type PrismaService } from '@modules/shared';
import { ListingApprovedEvent } from '@modules/admin';
import { LoggerService, PrismaService } from '@modules/shared';
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
@Injectable()

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type CommandBus } from '@nestjs/cqrs';
import { CommandBus } from '@nestjs/cqrs';
import { OnEvent } from '@nestjs/event-emitter';
import { type ListingRejectedEvent } from '@modules/admin';
import { type LoggerService, type PrismaService } from '@modules/shared';
import { ListingRejectedEvent } from '@modules/admin';
import { LoggerService, PrismaService } from '@modules/shared';
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
@Injectable()

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type CommandBus } from '@nestjs/cqrs';
import { CommandBus } from '@nestjs/cqrs';
import { OnEvent } from '@nestjs/event-emitter';
import { type ListingSoldEvent } from '@modules/listings';
import { type LoggerService, type PrismaService } from '@modules/shared';
import { ListingSoldEvent } from '@modules/listings';
import { LoggerService, PrismaService } from '@modules/shared';
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
@Injectable()

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type CommandBus } from '@nestjs/cqrs';
import { CommandBus } from '@nestjs/cqrs';
import { OnEvent } from '@nestjs/event-emitter';
import { type PaymentCompletedEvent } from '@modules/payments';
import { type LoggerService, type PrismaService } from '@modules/shared';
import { PaymentCompletedEvent } from '@modules/payments';
import { LoggerService, PrismaService } from '@modules/shared';
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
@Injectable()

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type CommandBus } from '@nestjs/cqrs';
import { CommandBus } from '@nestjs/cqrs';
import { OnEvent } from '@nestjs/event-emitter';
import { type PaymentFailedEvent } from '@modules/payments';
import { type LoggerService, type PrismaService } from '@modules/shared';
import { PaymentFailedEvent } from '@modules/payments';
import { LoggerService, PrismaService } from '@modules/shared';
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
@Injectable()

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type CommandBus } from '@nestjs/cqrs';
import { CommandBus } from '@nestjs/cqrs';
import { OnEvent } from '@nestjs/event-emitter';
import { type PaymentRefundedEvent } from '@modules/payments';
import { type LoggerService, type PrismaService } from '@modules/shared';
import { PaymentRefundedEvent } from '@modules/payments';
import { LoggerService, PrismaService } from '@modules/shared';
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
@Injectable()

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type CommandBus } from '@nestjs/cqrs';
import { CommandBus } from '@nestjs/cqrs';
import { OnEvent } from '@nestjs/event-emitter';
import { type LoggerService, type PrismaService } from '@modules/shared';
import { type QuotaExceededEvent } from '@modules/subscriptions';
import { LoggerService, PrismaService } from '@modules/shared';
import { QuotaExceededEvent } from '@modules/subscriptions';
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
@Injectable()

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type CommandBus } from '@nestjs/cqrs';
import { CommandBus } from '@nestjs/cqrs';
import { OnEvent } from '@nestjs/event-emitter';
import { type LoggerService, type PrismaService } from '@modules/shared';
import { type SubscriptionExpiredEvent } from '@modules/subscriptions';
import { LoggerService, PrismaService } from '@modules/shared';
import { SubscriptionExpiredEvent } from '@modules/subscriptions';
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
@Injectable()

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type CommandBus } from '@nestjs/cqrs';
import { CommandBus } from '@nestjs/cqrs';
import { OnEvent } from '@nestjs/event-emitter';
import { type LoggerService, type PrismaService } from '@modules/shared';
import { type SubscriptionCancelledEvent } from '@modules/subscriptions';
import { LoggerService, PrismaService } from '@modules/shared';
import { SubscriptionCancelledEvent } from '@modules/subscriptions';
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
@Injectable()

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type CommandBus } from '@nestjs/cqrs';
import { CommandBus } from '@nestjs/cqrs';
import { OnEvent } from '@nestjs/event-emitter';
import { type LoggerService, type PrismaService } from '@modules/shared';
import { type SubscriptionRenewedEvent } from '@modules/subscriptions';
import { LoggerService, PrismaService } from '@modules/shared';
import { SubscriptionRenewedEvent } from '@modules/subscriptions';
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
@Injectable()

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type CommandBus } from '@nestjs/cqrs';
import { CommandBus } from '@nestjs/cqrs';
import { OnEvent } from '@nestjs/event-emitter';
import { type UserKycUpdatedEvent } from '@modules/auth';
import { type LoggerService, type PrismaService } from '@modules/shared';
import { UserKycUpdatedEvent } from '@modules/auth';
import { LoggerService, PrismaService } from '@modules/shared';
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
@Injectable()

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type CommandBus } from '@nestjs/cqrs';
import { CommandBus } from '@nestjs/cqrs';
import { OnEvent } from '@nestjs/event-emitter';
import { type UserRegisteredEvent } from '@modules/auth';
import { type LoggerService, type PrismaService } from '@modules/shared';
import { UserRegisteredEvent } from '@modules/auth';
import { LoggerService, PrismaService } from '@modules/shared';
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
@Injectable()

View File

@@ -1,4 +1,4 @@
import { type NotificationChannel } from '../value-objects/notification-channel.vo';
import { NotificationChannel } from '../value-objects/notification-channel.vo';
export interface NotificationPreferenceEntity {
id: string;

View File

@@ -1,4 +1,4 @@
import { type NotificationChannel } from '../value-objects/notification-channel.vo';
import { NotificationChannel } from '../value-objects/notification-channel.vo';
export type NotificationStatus = 'PENDING' | 'SENT' | 'FAILED' | 'DELIVERED';

View File

@@ -1,5 +1,5 @@
import { type DomainEvent } from '@modules/shared';
import { type NotificationChannel } from '../value-objects/notification-channel.vo';
import { DomainEvent } from '@modules/shared';
import { NotificationChannel } from '../value-objects/notification-channel.vo';
export class NotificationSentEvent implements DomainEvent {
readonly eventName = 'notification.sent';

View File

@@ -3,12 +3,12 @@ export type { NotificationPreferenceEntity } from './entities/notification-prefe
export { NotificationSentEvent } from './events/notification-sent.event';
export {
NOTIFICATION_REPOSITORY,
type INotificationRepository,
INotificationRepository,
type CreateNotificationDto,
} from './repositories/notification.repository';
export {
NOTIFICATION_PREFERENCE_REPOSITORY,
type INotificationPreferenceRepository,
INotificationPreferenceRepository,
} from './repositories/notification-preference.repository';
export {
NotificationChannel,

View File

@@ -1,5 +1,5 @@
import { type NotificationPreferenceEntity } from '../entities/notification-preference.entity';
import { type NotificationChannel } from '../value-objects/notification-channel.vo';
import { NotificationPreferenceEntity } from '../entities/notification-preference.entity';
import { NotificationChannel } from '../value-objects/notification-channel.vo';
export const NOTIFICATION_PREFERENCE_REPOSITORY = Symbol('NOTIFICATION_PREFERENCE_REPOSITORY');

View File

@@ -1,5 +1,5 @@
import { type NotificationEntity, type NotificationStatus } from '../entities/notification.entity';
import { type NotificationChannel } from '../value-objects/notification-channel.vo';
import { NotificationEntity, NotificationStatus } from '../entities/notification.entity';
import { NotificationChannel } from '../value-objects/notification-channel.vo';
export const NOTIFICATION_REPOSITORY = Symbol('NOTIFICATION_REPOSITORY');

View File

@@ -1,4 +1,4 @@
import { type NotificationChannel as PrismaChannel } from '@prisma/client';
import { NotificationChannel as PrismaChannel } from '@prisma/client';
export const NotificationChannel = {
EMAIL: 'EMAIL',

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type PrismaService } from '@modules/shared';
import { type NotificationPreferenceEntity } from '../../domain/entities/notification-preference.entity';
import { type INotificationPreferenceRepository } from '../../domain/repositories/notification-preference.repository';
import { type NotificationChannel } from '../../domain/value-objects/notification-channel.vo';
import { PrismaService } from '@modules/shared';
import { NotificationPreferenceEntity } from '../../domain/entities/notification-preference.entity';
import { INotificationPreferenceRepository } from '../../domain/repositories/notification-preference.repository';
import { NotificationChannel } from '../../domain/value-objects/notification-channel.vo';
@Injectable()
export class PrismaNotificationPreferenceRepository implements INotificationPreferenceRepository {

View File

@@ -1,9 +1,9 @@
import { Injectable } from '@nestjs/common';
import { type Prisma } from '@prisma/client';
import { type PrismaService } from '@modules/shared';
import { type NotificationEntity, type NotificationStatus } from '../../domain/entities/notification.entity';
import { Prisma } from '@prisma/client';
import { PrismaService } from '@modules/shared';
import { NotificationEntity, NotificationStatus } from '../../domain/entities/notification.entity';
import {
type INotificationRepository,
INotificationRepository,
type CreateNotificationDto,
} from '../../domain/repositories/notification.repository';

View File

@@ -1,6 +1,6 @@
import { Injectable, type OnModuleInit } from '@nestjs/common';
import { Injectable, OnModuleInit } from '@nestjs/common';
import * as nodemailer from 'nodemailer';
import { type LoggerService } from '@modules/shared';
import { LoggerService } from '@modules/shared';
export interface SendEmailDto {
to: string;

View File

@@ -1,4 +1,4 @@
import { Injectable, type OnModuleInit } from '@nestjs/common';
import { Injectable, OnModuleInit } from '@nestjs/common';
import {
apps,
initializeApp,
@@ -6,7 +6,7 @@ import {
messaging,
type ServiceAccount,
} from 'firebase-admin';
import { type LoggerService } from '@modules/shared';
import { LoggerService } from '@modules/shared';
export interface SendPushDto {
token: string;

View File

@@ -13,14 +13,14 @@ import { AuthGuard } from '@nestjs/passport';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiProperty } from '@nestjs/swagger';
import { NotificationChannel as PrismaChannel } from '@prisma/client';
import { IsBoolean, IsEnum, IsString } from 'class-validator';
import { CurrentUser, type JwtPayload } from '@modules/auth';
import { CurrentUser, JwtPayload } from '@modules/auth';
import {
NOTIFICATION_REPOSITORY,
type INotificationRepository,
INotificationRepository,
NOTIFICATION_PREFERENCE_REPOSITORY,
type INotificationPreferenceRepository,
INotificationPreferenceRepository,
} from '../../domain';
import { type TemplateService } from '../../infrastructure/services/template.service';
import { TemplateService } from '../../infrastructure/services/template.service';
class UpdatePreferenceDto {
@ApiProperty({ enum: PrismaChannel, description: 'Notification channel' })