feat(industrial): add IndustrialListing CRUD endpoints + Typesense indexing

Wire full DDD stack for IndustrialListing: domain entity, repository interface,
CQRS commands/queries with handlers, Prisma repository, Typesense sync on
create/update/delete, controller with 5 REST endpoints, and validated DTOs.
Register all providers in IndustrialModule.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-16 17:08:08 +07:00
parent 13bd76ac5d
commit 8f2d325d60
19 changed files with 1605 additions and 2 deletions

View File

@@ -0,0 +1,177 @@
import { type IndustrialLeaseType, type IndustrialListingStatus, type IndustrialPropertyType } from '@prisma/client';
import { AggregateRoot } from '@modules/shared';
export interface IndustrialListingProps {
parkId: string;
agentId: string | null;
sellerId: string;
propertyType: IndustrialPropertyType;
leaseType: IndustrialLeaseType;
status: IndustrialListingStatus;
title: string;
description: string | null;
areaM2: number;
ceilingHeightM: number | null;
floorLoadTonM2: number | null;
columnSpacingM: number | null;
dockCount: number | null;
craneCapacityTon: number | null;
hasMezzanine: boolean;
hasOfficeArea: boolean;
officeAreaM2: number | null;
priceUsdM2: number | null;
pricingUnit: string | null;
totalLeasePrice: number | null;
managementFee: number | null;
depositMonths: number | null;
minLeaseYears: number | null;
maxLeaseYears: number | null;
leaseExpiry: Date | null;
availableFrom: Date | null;
powerCapacityKva: number | null;
waterSupplyM3Day: number | null;
media: Record<string, unknown>[] | null;
viewCount: number;
inquiryCount: number;
publishedAt: Date | null;
}
export class IndustrialListingEntity extends AggregateRoot<string> {
private _parkId: string;
private _agentId: string | null;
private _sellerId: string;
private _propertyType: IndustrialPropertyType;
private _leaseType: IndustrialLeaseType;
private _status: IndustrialListingStatus;
private _title: string;
private _description: string | null;
private _areaM2: number;
private _ceilingHeightM: number | null;
private _floorLoadTonM2: number | null;
private _columnSpacingM: number | null;
private _dockCount: number | null;
private _craneCapacityTon: number | null;
private _hasMezzanine: boolean;
private _hasOfficeArea: boolean;
private _officeAreaM2: number | null;
private _priceUsdM2: number | null;
private _pricingUnit: string | null;
private _totalLeasePrice: number | null;
private _managementFee: number | null;
private _depositMonths: number | null;
private _minLeaseYears: number | null;
private _maxLeaseYears: number | null;
private _leaseExpiry: Date | null;
private _availableFrom: Date | null;
private _powerCapacityKva: number | null;
private _waterSupplyM3Day: number | null;
private _media: Record<string, unknown>[] | null;
private _viewCount: number;
private _inquiryCount: number;
private _publishedAt: Date | null;
constructor(id: string, props: IndustrialListingProps, createdAt: Date, updatedAt: Date) {
super(id, createdAt, updatedAt);
this._parkId = props.parkId;
this._agentId = props.agentId;
this._sellerId = props.sellerId;
this._propertyType = props.propertyType;
this._leaseType = props.leaseType;
this._status = props.status;
this._title = props.title;
this._description = props.description;
this._areaM2 = props.areaM2;
this._ceilingHeightM = props.ceilingHeightM;
this._floorLoadTonM2 = props.floorLoadTonM2;
this._columnSpacingM = props.columnSpacingM;
this._dockCount = props.dockCount;
this._craneCapacityTon = props.craneCapacityTon;
this._hasMezzanine = props.hasMezzanine;
this._hasOfficeArea = props.hasOfficeArea;
this._officeAreaM2 = props.officeAreaM2;
this._priceUsdM2 = props.priceUsdM2;
this._pricingUnit = props.pricingUnit;
this._totalLeasePrice = props.totalLeasePrice;
this._managementFee = props.managementFee;
this._depositMonths = props.depositMonths;
this._minLeaseYears = props.minLeaseYears;
this._maxLeaseYears = props.maxLeaseYears;
this._leaseExpiry = props.leaseExpiry;
this._availableFrom = props.availableFrom;
this._powerCapacityKva = props.powerCapacityKva;
this._waterSupplyM3Day = props.waterSupplyM3Day;
this._media = props.media;
this._viewCount = props.viewCount;
this._inquiryCount = props.inquiryCount;
this._publishedAt = props.publishedAt;
}
get parkId() { return this._parkId; }
get agentId() { return this._agentId; }
get sellerId() { return this._sellerId; }
get propertyType() { return this._propertyType; }
get leaseType() { return this._leaseType; }
get status() { return this._status; }
get title() { return this._title; }
get description() { return this._description; }
get areaM2() { return this._areaM2; }
get ceilingHeightM() { return this._ceilingHeightM; }
get floorLoadTonM2() { return this._floorLoadTonM2; }
get columnSpacingM() { return this._columnSpacingM; }
get dockCount() { return this._dockCount; }
get craneCapacityTon() { return this._craneCapacityTon; }
get hasMezzanine() { return this._hasMezzanine; }
get hasOfficeArea() { return this._hasOfficeArea; }
get officeAreaM2() { return this._officeAreaM2; }
get priceUsdM2() { return this._priceUsdM2; }
get pricingUnit() { return this._pricingUnit; }
get totalLeasePrice() { return this._totalLeasePrice; }
get managementFee() { return this._managementFee; }
get depositMonths() { return this._depositMonths; }
get minLeaseYears() { return this._minLeaseYears; }
get maxLeaseYears() { return this._maxLeaseYears; }
get leaseExpiry() { return this._leaseExpiry; }
get availableFrom() { return this._availableFrom; }
get powerCapacityKva() { return this._powerCapacityKva; }
get waterSupplyM3Day() { return this._waterSupplyM3Day; }
get media() { return this._media; }
get viewCount() { return this._viewCount; }
get inquiryCount() { return this._inquiryCount; }
get publishedAt() { return this._publishedAt; }
updateDetails(props: Partial<Omit<IndustrialListingProps, 'parkId' | 'sellerId'>>): void {
if (props.agentId !== undefined) this._agentId = props.agentId;
if (props.propertyType !== undefined) this._propertyType = props.propertyType;
if (props.leaseType !== undefined) this._leaseType = props.leaseType;
if (props.status !== undefined) this._status = props.status;
if (props.title !== undefined) this._title = props.title;
if (props.description !== undefined) this._description = props.description;
if (props.areaM2 !== undefined) this._areaM2 = props.areaM2;
if (props.ceilingHeightM !== undefined) this._ceilingHeightM = props.ceilingHeightM;
if (props.floorLoadTonM2 !== undefined) this._floorLoadTonM2 = props.floorLoadTonM2;
if (props.columnSpacingM !== undefined) this._columnSpacingM = props.columnSpacingM;
if (props.dockCount !== undefined) this._dockCount = props.dockCount;
if (props.craneCapacityTon !== undefined) this._craneCapacityTon = props.craneCapacityTon;
if (props.hasMezzanine !== undefined) this._hasMezzanine = props.hasMezzanine;
if (props.hasOfficeArea !== undefined) this._hasOfficeArea = props.hasOfficeArea;
if (props.officeAreaM2 !== undefined) this._officeAreaM2 = props.officeAreaM2;
if (props.priceUsdM2 !== undefined) this._priceUsdM2 = props.priceUsdM2;
if (props.pricingUnit !== undefined) this._pricingUnit = props.pricingUnit;
if (props.totalLeasePrice !== undefined) this._totalLeasePrice = props.totalLeasePrice;
if (props.managementFee !== undefined) this._managementFee = props.managementFee;
if (props.depositMonths !== undefined) this._depositMonths = props.depositMonths;
if (props.minLeaseYears !== undefined) this._minLeaseYears = props.minLeaseYears;
if (props.maxLeaseYears !== undefined) this._maxLeaseYears = props.maxLeaseYears;
if (props.leaseExpiry !== undefined) this._leaseExpiry = props.leaseExpiry;
if (props.availableFrom !== undefined) this._availableFrom = props.availableFrom;
if (props.powerCapacityKva !== undefined) this._powerCapacityKva = props.powerCapacityKva;
if (props.waterSupplyM3Day !== undefined) this._waterSupplyM3Day = props.waterSupplyM3Day;
if (props.media !== undefined) this._media = props.media;
this.updatedAt = new Date();
}
softDelete(): void {
this._status = 'EXPIRED' as IndustrialListingStatus;
this.updatedAt = new Date();
}
}