feat: add pricing checkout flow, MFA type fixes, and Wave 13 audit docs

- Pricing page: enhanced with checkout modal integration, plan
  comparison table, and subscription funnel
- Payment return page: new VNPay/MoMo callback handler
- Subscription components: new checkout-modal with payment method
  selection (VNPay, MoMo, ZaloPay)
- API modules: type-safe PII encryption, improved error handling in
  MFA/auth/payments/analytics/search/notifications modules
- Audit docs: comprehensive Wave 13 platform assessment, pricing
  audit, production readiness checklist
- Updated PROJECT_TRACKER with Wave 13 status

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-12 20:17:11 +07:00
parent 51c4ecbf4e
commit db7147a95d
66 changed files with 6530 additions and 283 deletions

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { type CommandBus } from '@nestjs/cqrs';
import { OnEvent } from '@nestjs/event-emitter';
import { type LoggerService } from '@modules/shared';
import { LoggerService } from '@modules/shared';
import { RecalculateQualityScoreCommand } from '../commands/recalculate-quality-score/recalculate-quality-score.command';
@Injectable()

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { type PrismaService } from '@modules/shared';
import { PrismaService } from '@modules/shared';
import { AgentEntity } from '../../domain/entities/agent.entity';
import {
type AgentDashboardData,

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { type MarketIndex as PrismaMarketIndex, type PropertyType } from '@prisma/client';
import { type PrismaService } from '@modules/shared';
import { PrismaService } from '@modules/shared';
import { MarketIndexEntity, type MarketIndexProps } from '../../domain/entities/market-index.entity';
import {
type IMarketIndexRepository,

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { type Prisma, type Valuation as PrismaValuation } from '@prisma/client';
import { type PrismaService } from '@modules/shared';
import { PrismaService } from '@modules/shared';
import { ValuationEntity, type ValuationProps } from '../../domain/entities/valuation.entity';
import { type IValuationRepository } from '../../domain/repositories/valuation.repository';

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { type LoggerService } from '@modules/shared';
import { LoggerService } from '@modules/shared';
export interface AiPredictRequest {
area: number;

View File

@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { type PrismaService, type LoggerService } from '@modules/shared';
import { PrismaService, LoggerService } from '@modules/shared';
import {
type IAVMService,
type AVMParams,
@@ -11,7 +11,7 @@ import {
type IAiServiceClient,
type AiPredictRequest,
} from './ai-service.client';
import { type PrismaAVMService } from './prisma-avm.service';
import { PrismaAVMService } from './prisma-avm.service';
@Injectable()
export class HttpAVMService implements IAVMService {

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { type PropertyType } from '@prisma/client';
import { type PrismaService } from '@modules/shared';
import { PrismaService } from '@modules/shared';
import {
type IAVMService,
type AVMParams,

View File

@@ -1,8 +1,8 @@
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { DomainException, type LoggerService, UnauthorizedException, ValidationException } from '@modules/shared';
import { DomainException, LoggerService, UnauthorizedException, ValidationException } from '@modules/shared';
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
import { type MfaService } from '../../../infrastructure/services/mfa.service';
import { MfaService } from '../../../infrastructure/services/mfa.service';
import { DisableMfaCommand } from './disable-mfa.command';
@CommandHandler(DisableMfaCommand)

View File

@@ -1,13 +1,13 @@
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { DomainException, type LoggerService, UnauthorizedException } from '@modules/shared';
import { DomainException, LoggerService, UnauthorizedException } from '@modules/shared';
import {
MFA_CHALLENGE_REPOSITORY,
type IMfaChallengeRepository,
} from '../../../domain/repositories/mfa-challenge.repository';
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
import { type MfaService } from '../../../infrastructure/services/mfa.service';
import { type TokenService, type TokenPair } from '../../../infrastructure/services/token.service';
import { MfaService } from '../../../infrastructure/services/mfa.service';
import { TokenService, type TokenPair } from '../../../infrastructure/services/token.service';
import { UseBackupCodeCommand } from './use-backup-code.command';
@CommandHandler(UseBackupCodeCommand)

View File

@@ -1,13 +1,13 @@
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { DomainException, type LoggerService, UnauthorizedException } from '@modules/shared';
import { DomainException, LoggerService, UnauthorizedException } from '@modules/shared';
import {
MFA_CHALLENGE_REPOSITORY,
type IMfaChallengeRepository,
} from '../../../domain/repositories/mfa-challenge.repository';
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
import { type MfaService } from '../../../infrastructure/services/mfa.service';
import { type TokenService, type TokenPair } from '../../../infrastructure/services/token.service';
import { MfaService } from '../../../infrastructure/services/mfa.service';
import { TokenService, type TokenPair } from '../../../infrastructure/services/token.service';
import { VerifyMfaChallengeCommand } from './verify-mfa-challenge.command';
@CommandHandler(VerifyMfaChallengeCommand)

View File

@@ -1,8 +1,8 @@
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { DomainException, type LoggerService, ValidationException } from '@modules/shared';
import { DomainException, LoggerService, ValidationException } from '@modules/shared';
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
import { type MfaService } from '../../../infrastructure/services/mfa.service';
import { MfaService } from '../../../infrastructure/services/mfa.service';
import { VerifyMfaSetupCommand } from './verify-mfa-setup.command';
export interface VerifyMfaSetupResultDto {

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { type PrismaService } from '@modules/shared';
import { PrismaService } from '@modules/shared';
import {
type IMfaChallengeRepository,
type MfaChallengeRecord,

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { type PrismaService } from '@modules/shared';
import { PrismaService } from '@modules/shared';
import {
type IRefreshTokenRepository,
type RefreshTokenRecord,

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { type Prisma, type User as PrismaUser } from '@prisma/client';
import { type PrismaService } from '@modules/shared';
import { PrismaService } from '@modules/shared';
import { UserEntity, type UserProps } from '../../domain/entities/user.entity';
import { type IUserRepository } from '../../domain/repositories/user.repository';
import { Email } from '../../domain/value-objects/email.vo';

View File

@@ -1,6 +1,6 @@
import { randomBytes, createHash } from 'crypto';
import { Inject, Injectable } from '@nestjs/common';
import { type JwtService } from '@nestjs/jwt';
import { JwtService } from '@nestjs/jwt';
import {
REFRESH_TOKEN_REPOSITORY,
type IRefreshTokenRepository,

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { type LoggerService } from '@modules/shared';
import { LoggerService } from '@modules/shared';
import { type OAuthService, type OAuthUserProfile } from '../services/oauth.service';
import { type TokenPair } from '../services/token.service';

View File

@@ -1,7 +1,7 @@
import { Injectable, type CanActivate, type ExecutionContext } from '@nestjs/common';
import { type Reflector } from '@nestjs/core';
import { type UserRole } from '@prisma/client';
import { type LoggerService } from '@modules/shared';
import { LoggerService } from '@modules/shared';
import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable()

View File

@@ -1,5 +1,5 @@
import { type Prisma } from '@prisma/client';
import { type PrismaService } from '@modules/shared';
import { PrismaService } from '@modules/shared';
import { type ListingDetailData, type ListingSearchItem, type ListingSellerItem } from '../../domain/repositories/listing-read.dto';
import { type ListingSearchParams, type PaginatedResult } from '../../domain/repositories/listing.repository';

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { type Listing as PrismaListing, type ListingStatus } from '@prisma/client';
import { type PrismaService } from '@modules/shared';
import { PrismaService } from '@modules/shared';
import { ListingEntity, type ListingProps } from '../../domain/entities/listing.entity';
import { type ListingDetailData, type ListingSearchItem, type ListingSellerItem } from '../../domain/repositories/listing-read.dto';
import { type IListingRepository, type ListingSearchParams, type PaginatedResult } from '../../domain/repositories/listing.repository';

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { type Prisma, type Property as PrismaProperty, type PropertyMedia as PrismaMedia } from '@prisma/client';
import { type PrismaService } from '@modules/shared';
import { PrismaService } from '@modules/shared';
import { PropertyMediaEntity, type PropertyMediaProps } from '../../domain/entities/property-media.entity';
import { PropertyEntity, type PropertyProps } from '../../domain/entities/property.entity';
import { type IPropertyRepository } from '../../domain/repositories/property.repository';

View File

@@ -9,7 +9,7 @@ import {
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Injectable, type OnModuleInit } from '@nestjs/common';
import { type LoggerService } from '@modules/shared';
import { LoggerService } from '@modules/shared';
export const MEDIA_STORAGE_SERVICE = Symbol('MEDIA_STORAGE_SERVICE');

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { type PropertyType } from '@prisma/client';
import { type PrismaService } from '@modules/shared';
import { PrismaService } from '@modules/shared';
import {
type DuplicateCandidate,
type DuplicateCheckParams,

View File

@@ -1,6 +1,6 @@
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { DomainException, type EventBusService, type LoggerService } from '@modules/shared';
import { DomainException, EventBusService, LoggerService } from '@modules/shared';
import { NotificationSentEvent } from '../../../domain/events/notification-sent.event';
import {
NOTIFICATION_PREFERENCE_REPOSITORY,
@@ -10,9 +10,9 @@ import {
NOTIFICATION_REPOSITORY,
type 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,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { type PrismaService } from '@modules/shared';
import { 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';

View File

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

View File

@@ -1,6 +1,6 @@
import { Injectable, type 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

@@ -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

@@ -20,7 +20,7 @@ import {
NOTIFICATION_PREFERENCE_REPOSITORY,
type 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' })

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { Prisma, type Payment as PrismaPayment, type PaymentStatus } from '@prisma/client';
import { type PrismaService } from '@modules/shared';
import { PrismaService } from '@modules/shared';
import { PaymentEntity, type PaymentProps } from '../../domain/entities/payment.entity';
import { type IPaymentRepository } from '../../domain/repositories/payment.repository';
import { Money } from '../../domain/value-objects/money.vo';

View File

@@ -1,8 +1,8 @@
import * as crypto from 'crypto';
import { Injectable } from '@nestjs/common';
import { type ConfigService } from '@nestjs/config';
import { ConfigService } from '@nestjs/config';
import { type PaymentProvider } from '@prisma/client';
import { type LoggerService } from '@modules/shared';
import { LoggerService } from '@modules/shared';
import {
type IPaymentGateway,
type CreatePaymentUrlParams,

View File

@@ -1,12 +1,12 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { type PaymentProvider } from '@prisma/client';
import { type MomoService } from './momo.service';
import { MomoService } from './momo.service';
import {
type IPaymentGateway,
type IPaymentGatewayFactory,
} from './payment-gateway.interface';
import { type VnpayService } from './vnpay.service';
import { type ZalopayService } from './zalopay.service';
import { VnpayService } from './vnpay.service';
import { ZalopayService } from './zalopay.service';
@Injectable()
export class PaymentGatewayFactory implements IPaymentGatewayFactory {

View File

@@ -1,8 +1,8 @@
import * as crypto from 'crypto';
import { Injectable } from '@nestjs/common';
import { type ConfigService } from '@nestjs/config';
import { ConfigService } from '@nestjs/config';
import { type PaymentProvider } from '@prisma/client';
import { type LoggerService } from '@modules/shared';
import { LoggerService } from '@modules/shared';
import {
type IPaymentGateway,
type CreatePaymentUrlParams,

View File

@@ -1,8 +1,8 @@
import * as crypto from 'crypto';
import { Injectable } from '@nestjs/common';
import { type ConfigService } from '@nestjs/config';
import { ConfigService } from '@nestjs/config';
import { type PaymentProvider } from '@prisma/client';
import { type LoggerService } from '@modules/shared';
import { LoggerService } from '@modules/shared';
import {
type IPaymentGateway,
type CreatePaymentUrlParams,

View File

@@ -1,7 +1,7 @@
import { InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { DomainException, type LoggerService } from '@modules/shared';
import { type ListingIndexerService } from '../../../infrastructure/services/listing-indexer.service';
import { DomainException, LoggerService } from '@modules/shared';
import { ListingIndexerService } from '../../../infrastructure/services/listing-indexer.service';
import { ReindexAllCommand } from './reindex-all.command';
export interface ReindexResult {

View File

@@ -1,7 +1,7 @@
import { InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { DomainException, type LoggerService } from '@modules/shared';
import { type ListingIndexerService } from '../../../infrastructure/services/listing-indexer.service';
import { DomainException, LoggerService } from '@modules/shared';
import { ListingIndexerService } from '../../../infrastructure/services/listing-indexer.service';
import { SyncListingCommand } from './sync-listing.command';
@CommandHandler(SyncListingCommand)

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { CacheService, CachePrefix, type LoggerService } from '@modules/shared';
import { type ListingIndexerService } from '../services/listing-indexer.service';
import { CacheService, CachePrefix, LoggerService } from '@modules/shared';
import { ListingIndexerService } from '../services/listing-indexer.service';
@Injectable()
export class ListingApprovedEventHandler {

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { type ListingStatusChangedEvent } from '@modules/listings';
import { CacheService, CachePrefix, type LoggerService } from '@modules/shared';
import { type ListingIndexerService } from '../services/listing-indexer.service';
import { CacheService, CachePrefix, LoggerService } from '@modules/shared';
import { ListingIndexerService } from '../services/listing-indexer.service';
@Injectable()
export class ListingStatusChangedHandler {

View File

@@ -1,6 +1,6 @@
import { Injectable, type OnModuleInit } from '@nestjs/common';
import { Client as TypesenseClient } from 'typesense';
import { type LoggerService } from '@modules/shared';
import { LoggerService } from '@modules/shared';
@Injectable()
export class TypesenseClientService implements OnModuleInit {

View File

@@ -1,14 +1,14 @@
import { Injectable } from '@nestjs/common';
import { type Client as TypesenseClient } from 'typesense';
import { type CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
import { type LoggerService } from '@modules/shared';
import { LoggerService } from '@modules/shared';
import {
type ISearchRepository,
type ListingDocument,
type SearchParams,
type SearchResult,
} from '../../domain/repositories/search.repository';
import { type TypesenseClientService } from './typesense-client.service';
import { TypesenseClientService } from './typesense-client.service';
const COLLECTION_NAME = 'listings';

View File

@@ -1,7 +1,7 @@
import { Module, type OnModuleInit } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { makeCounterProvider } from '@willsoto/nestjs-prometheus';
import { type LoggerService } from '@modules/shared';
import { LoggerService } from '@modules/shared';
import { CreateSavedSearchHandler } from './application/commands/create-saved-search/create-saved-search.handler';
import { DeleteSavedSearchHandler } from './application/commands/delete-saved-search/delete-saved-search.handler';
import { ReindexAllHandler } from './application/commands/reindex-all/reindex-all.handler';

View File

@@ -16,7 +16,7 @@ import {
isEncrypted,
type FieldEncryptionConfig,
} from './field-encryption';
import { type LoggerService } from './logger.service';
import { LoggerService } from './logger.service';
// ---------------------------------------------------------------------------
// Configuration types

View File

@@ -9,7 +9,7 @@ import { Prisma } from '@prisma/client';
import { type Request, type Response } from 'express';
import { DomainException, type ErrorResponseBody } from '../../domain/domain-exception';
import { ErrorCode } from '../../domain/error-codes';
import { type LoggerService } from '../logger.service';
import { LoggerService } from '../logger.service';
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {

View File

@@ -11,8 +11,8 @@ import {
ENDPOINT_RATE_LIMIT_KEY,
type EndpointRateLimitOptions,
} from '../decorators/endpoint-rate-limit.decorator';
import { type LoggerService } from '../logger.service';
import { type RedisService } from '../redis.service';
import { LoggerService } from '../logger.service';
import { RedisService } from '../redis.service';
/** Express request extended with optional JWT user payload. */
interface AuthenticatedRequest extends Request {

View File

@@ -7,8 +7,8 @@ import {
} from '@nestjs/common';
import { type Reflector } from '@nestjs/core';
import { type UserRole } from '@prisma/client';
import { type LoggerService } from '../logger.service';
import { type RedisService } from '../redis.service';
import { LoggerService } from '../logger.service';
import { RedisService } from '../redis.service';
/**
* Role-based rate limits (requests per window).

View File

@@ -1,6 +1,6 @@
import { Injectable, type NestMiddleware } from '@nestjs/common';
import { type NextFunction, type Request, type Response } from 'express';
import { type LoggerService } from '../logger.service';
import { LoggerService } from '../logger.service';
@Injectable()
export class RequestLoggingMiddleware implements NestMiddleware {

View File

@@ -4,7 +4,7 @@ import { PrismaClient } from '@prisma/client';
import pg from 'pg';
import { createEncryptionExtension } from './encryption-middleware';
import { FieldEncryptionService } from './field-encryption.service';
import { type LoggerService } from './logger.service';
import { LoggerService } from './logger.service';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {