feat(api): add inquiries, leads, and agents modules for Agent Portal
Build three new DDD modules following existing CQRS patterns: - Inquiries: CRUD endpoints for buyer consultation requests with agent notification support - Leads: Full lead lifecycle management with status state machine and conversion tracking - Agents: Quality score calculation (event-driven on review changes) and dashboard stats API All modules include unit tests (14 test files, all 797 tests pass). Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
101
apps/api/src/modules/leads/domain/entities/lead.entity.ts
Normal file
101
apps/api/src/modules/leads/domain/entities/lead.entity.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { AggregateRoot, ValidationException } from '@modules/shared';
|
||||
import { LeadCreatedEvent } from '../events/lead-created.event';
|
||||
import { LeadStatusChangedEvent } from '../events/lead-status-changed.event';
|
||||
import { type LeadScore } from '../value-objects/lead-score.vo';
|
||||
|
||||
export type LeadStatus = 'NEW' | 'CONTACTED' | 'QUALIFIED' | 'NEGOTIATING' | 'CONVERTED' | 'LOST';
|
||||
|
||||
const VALID_TRANSITIONS: Record<LeadStatus, LeadStatus[]> = {
|
||||
NEW: ['CONTACTED', 'LOST'],
|
||||
CONTACTED: ['QUALIFIED', 'LOST'],
|
||||
QUALIFIED: ['NEGOTIATING', 'LOST'],
|
||||
NEGOTIATING: ['CONVERTED', 'LOST'],
|
||||
CONVERTED: [],
|
||||
LOST: [],
|
||||
};
|
||||
|
||||
export interface LeadProps {
|
||||
agentId: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
email: string | null;
|
||||
source: string;
|
||||
score: LeadScore | null;
|
||||
notes: unknown;
|
||||
status: LeadStatus;
|
||||
}
|
||||
|
||||
export class LeadEntity extends AggregateRoot<string> {
|
||||
private _agentId: string;
|
||||
private _name: string;
|
||||
private _phone: string;
|
||||
private _email: string | null;
|
||||
private _source: string;
|
||||
private _score: LeadScore | null;
|
||||
private _notes: unknown;
|
||||
private _status: LeadStatus;
|
||||
|
||||
constructor(id: string, props: LeadProps, createdAt?: Date, updatedAt?: Date) {
|
||||
super(id, createdAt);
|
||||
if (updatedAt) this.updatedAt = updatedAt;
|
||||
this._agentId = props.agentId;
|
||||
this._name = props.name;
|
||||
this._phone = props.phone;
|
||||
this._email = props.email;
|
||||
this._source = props.source;
|
||||
this._score = props.score;
|
||||
this._notes = props.notes;
|
||||
this._status = props.status;
|
||||
}
|
||||
|
||||
get agentId(): string { return this._agentId; }
|
||||
get name(): string { return this._name; }
|
||||
get phone(): string { return this._phone; }
|
||||
get email(): string | null { return this._email; }
|
||||
get source(): string { return this._source; }
|
||||
get score(): LeadScore | null { return this._score; }
|
||||
get notes(): unknown { return this._notes; }
|
||||
get status(): LeadStatus { return this._status; }
|
||||
|
||||
static createNew(
|
||||
id: string,
|
||||
agentId: string,
|
||||
name: string,
|
||||
phone: string,
|
||||
email: string | null,
|
||||
source: string,
|
||||
score: LeadScore | null,
|
||||
notes: unknown,
|
||||
): LeadEntity {
|
||||
const lead = new LeadEntity(id, {
|
||||
agentId,
|
||||
name,
|
||||
phone,
|
||||
email,
|
||||
source,
|
||||
score,
|
||||
notes,
|
||||
status: 'NEW',
|
||||
});
|
||||
|
||||
lead.addDomainEvent(new LeadCreatedEvent(id, agentId));
|
||||
return lead;
|
||||
}
|
||||
|
||||
updateStatus(newStatus: LeadStatus): void {
|
||||
const allowed = VALID_TRANSITIONS[this._status];
|
||||
if (!allowed.includes(newStatus)) {
|
||||
throw new ValidationException(
|
||||
`Không thể chuyển trạng thái từ ${this._status} sang ${newStatus}`,
|
||||
);
|
||||
}
|
||||
|
||||
const oldStatus = this._status;
|
||||
this._status = newStatus;
|
||||
this.updatedAt = new Date();
|
||||
|
||||
this.addDomainEvent(
|
||||
new LeadStatusChangedEvent(this.id, this._agentId, oldStatus, newStatus),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user