feat(listings): add price validator, moderation service, and improve handlers
Add domain-level price validator and moderation services with Prisma implementation. Improve listing creation, status management, and media upload handlers. Add price validator spec. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { type ListingStatus, type TransactionType } from '@prisma/client';
|
||||
import { AggregateRoot } from '@modules/shared/domain/aggregate-root';
|
||||
import { ValidationException } from '@modules/shared/domain/domain-exception';
|
||||
import { AggregateRoot, ValidationException } from '@modules/shared';
|
||||
import { ListingApprovedEvent } from '../events/listing-approved.event';
|
||||
import { ListingCreatedEvent } from '../events/listing-created.event';
|
||||
import { ListingSoldEvent } from '../events/listing-sold.event';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity } from '@modules/shared/domain/base-entity';
|
||||
import { BaseEntity } from '@modules/shared';
|
||||
|
||||
export interface PropertyMediaProps {
|
||||
propertyId: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type PropertyType, type Direction } from '@prisma/client';
|
||||
import { AggregateRoot } from '@modules/shared/domain/aggregate-root';
|
||||
import { AggregateRoot } from '@modules/shared';
|
||||
import { type Address } from '../value-objects/address.vo';
|
||||
import { type GeoPoint } from '../value-objects/geo-point.vo';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type DomainEvent } from '@modules/shared/domain/domain-event';
|
||||
import { type DomainEvent } from '@modules/shared';
|
||||
|
||||
export class ListingApprovedEvent implements DomainEvent {
|
||||
readonly eventName = 'listing.approved';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type TransactionType } from '@prisma/client';
|
||||
import { type DomainEvent } from '@modules/shared/domain/domain-event';
|
||||
import { type DomainEvent } from '@modules/shared';
|
||||
|
||||
export class ListingCreatedEvent implements DomainEvent {
|
||||
readonly eventName = 'listing.created';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type ListingStatus } from '@prisma/client';
|
||||
import { type DomainEvent } from '@modules/shared/domain/domain-event';
|
||||
import { type DomainEvent } from '@modules/shared';
|
||||
|
||||
export class ListingSoldEvent implements DomainEvent {
|
||||
readonly eventName = 'listing.sold';
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { type ListingEntity } from '../entities/listing.entity';
|
||||
|
||||
export interface ModerationAction {
|
||||
action: 'approve' | 'reject';
|
||||
moderationScore?: number;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class ModerationService {
|
||||
/**
|
||||
* Apply a moderation action to a listing entity.
|
||||
* Mutates the listing in-place (score + status transition).
|
||||
*/
|
||||
applyModeration(listing: ListingEntity, moderation: ModerationAction): void {
|
||||
if (moderation.moderationScore !== undefined) {
|
||||
listing.setModerationScore(moderation.moderationScore, moderation.notes);
|
||||
}
|
||||
|
||||
if (moderation.action === 'approve') {
|
||||
listing.approve();
|
||||
} else {
|
||||
listing.reject(moderation.notes ?? 'Bị từ chối bởi moderator');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a status transition that may be moderation-related.
|
||||
* Detects approve/reject patterns and delegates to entity methods.
|
||||
*/
|
||||
applyStatusTransition(
|
||||
listing: ListingEntity,
|
||||
newStatus: string,
|
||||
moderationNotes?: string,
|
||||
): void {
|
||||
if (newStatus === 'REJECTED' && moderationNotes) {
|
||||
listing.reject(moderationNotes);
|
||||
} else if (newStatus === 'ACTIVE' && listing.status === 'PENDING_REVIEW') {
|
||||
listing.approve();
|
||||
} else {
|
||||
listing.transitionTo(newStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { type PropertyType } from '@prisma/client';
|
||||
|
||||
export const PRICE_VALIDATOR = Symbol('PRICE_VALIDATOR');
|
||||
|
||||
export interface PriceValidationParams {
|
||||
priceVND: bigint;
|
||||
areaM2: number;
|
||||
propertyType: PropertyType;
|
||||
district: string;
|
||||
}
|
||||
|
||||
export interface PriceValidationResult {
|
||||
isValid: boolean;
|
||||
isSuspicious: boolean;
|
||||
reason?: string;
|
||||
pricePerM2: number;
|
||||
minPricePerM2: number;
|
||||
maxPricePerM2: number;
|
||||
}
|
||||
|
||||
export interface IPriceValidator {
|
||||
/** Validate price range for a property type in a given district */
|
||||
validate(params: PriceValidationParams): Promise<PriceValidationResult>;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Result } from '@modules/shared/domain/result';
|
||||
import { ValueObject } from '@modules/shared/domain/value-object';
|
||||
import { Result, ValueObject } from '@modules/shared';
|
||||
|
||||
interface AddressProps {
|
||||
address: string;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Result } from '@modules/shared/domain/result';
|
||||
import { ValueObject } from '@modules/shared/domain/value-object';
|
||||
import { Result, ValueObject } from '@modules/shared';
|
||||
|
||||
interface GeoPointProps {
|
||||
latitude: number;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Result } from '@modules/shared/domain/result';
|
||||
import { ValueObject } from '@modules/shared/domain/value-object';
|
||||
import { Result, ValueObject } from '@modules/shared';
|
||||
|
||||
interface PriceProps {
|
||||
amountVND: bigint;
|
||||
|
||||
Reference in New Issue
Block a user