feat(messaging): add in-app messaging module with Conversation + Message models
Implements buyer-agent in-app messaging (Task 8.4): - Prisma models: Conversation, ConversationParticipant, Message - Full DDD module: domain entities, repository interfaces, CQRS commands/queries - REST API: POST/GET conversations, POST/GET messages, PATCH read, DELETE messages - WebSocket gateway (/messaging namespace): real-time message delivery, typing indicators, room-based routing - 46 unit tests covering handlers, repositories, controller, and gateway Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -880,3 +880,212 @@ model Review {
|
||||
@@index([userId])
|
||||
@@index([targetType, targetId, createdAt(sort: Desc)])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INDUSTRIAL PARKS (KCN)
|
||||
// =============================================================================
|
||||
|
||||
enum IndustrialParkStatus {
|
||||
PLANNING
|
||||
UNDER_CONSTRUCTION
|
||||
OPERATIONAL
|
||||
FULL
|
||||
}
|
||||
|
||||
enum IndustrialPropertyType {
|
||||
INDUSTRIAL_LAND
|
||||
READY_BUILT_FACTORY
|
||||
READY_BUILT_WAREHOUSE
|
||||
LOGISTICS_CENTER
|
||||
OFFICE_IN_PARK
|
||||
DATA_CENTER
|
||||
}
|
||||
|
||||
enum IndustrialLeaseType {
|
||||
LAND_LEASE
|
||||
FACTORY_LEASE
|
||||
WAREHOUSE_LEASE
|
||||
SUBLEASE
|
||||
}
|
||||
|
||||
enum IndustrialListingStatus {
|
||||
DRAFT
|
||||
ACTIVE
|
||||
RESERVED
|
||||
LEASED
|
||||
EXPIRED
|
||||
}
|
||||
|
||||
enum VietnamRegion {
|
||||
NORTH
|
||||
CENTRAL
|
||||
SOUTH
|
||||
}
|
||||
|
||||
model IndustrialPark {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
nameEn String?
|
||||
slug String @unique
|
||||
developer String
|
||||
operator String?
|
||||
status IndustrialParkStatus @default(PLANNING)
|
||||
location Unsupported("geometry(Point, 4326)")
|
||||
address String
|
||||
district String
|
||||
province String
|
||||
region VietnamRegion
|
||||
totalAreaHa Float
|
||||
leasableAreaHa Float
|
||||
occupancyRate Float @default(0) // 0-100
|
||||
remainingAreaHa Float
|
||||
tenantCount Int @default(0)
|
||||
establishedYear Int?
|
||||
landRentUsdM2Year Float?
|
||||
rbfRentUsdM2Month Float?
|
||||
rbwRentUsdM2Month Float?
|
||||
managementFeeUsd Float?
|
||||
infrastructure Json? // { electricity, water, wastewater, telecom, roads, fire }
|
||||
connectivity Json? // { nearestPort, airport, highway, railway, seaport }
|
||||
incentives Json? // { taxHoliday, importDuty, landRentReduction, specialZone }
|
||||
targetIndustries String[]
|
||||
existingTenants Json? // [{ name, country, industry }]
|
||||
certifications Json? // ["ISO 14001", "Green park"]
|
||||
media Json?
|
||||
documents Json?
|
||||
description String? @db.Text
|
||||
descriptionEn String? @db.Text
|
||||
isVerified Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
listings IndustrialListing[]
|
||||
|
||||
@@index([status])
|
||||
@@index([province])
|
||||
@@index([region])
|
||||
@@index([developer])
|
||||
@@index([location], type: Gist)
|
||||
@@index([isVerified])
|
||||
@@index([occupancyRate])
|
||||
@@index([landRentUsdM2Year])
|
||||
@@index([region, province, status])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
model IndustrialListing {
|
||||
id String @id @default(cuid())
|
||||
parkId String
|
||||
park IndustrialPark @relation(fields: [parkId], references: [id], onDelete: Cascade)
|
||||
agentId String?
|
||||
sellerId String
|
||||
propertyType IndustrialPropertyType
|
||||
leaseType IndustrialLeaseType
|
||||
status IndustrialListingStatus @default(DRAFT)
|
||||
title String
|
||||
description String? @db.Text
|
||||
areaM2 Float
|
||||
ceilingHeightM Float?
|
||||
floorLoadTonM2 Float?
|
||||
columnSpacingM Float?
|
||||
dockCount Int?
|
||||
craneCapacityTon Float?
|
||||
hasMezzanine Boolean @default(false)
|
||||
hasOfficeArea Boolean @default(false)
|
||||
officeAreaM2 Float?
|
||||
priceUsdM2 Float?
|
||||
pricingUnit String? // "usd/m2/month", "usd/m2/year"
|
||||
totalLeasePrice Float?
|
||||
managementFee Float?
|
||||
depositMonths Int?
|
||||
minLeaseYears Int?
|
||||
maxLeaseYears Int?
|
||||
leaseExpiry DateTime?
|
||||
availableFrom DateTime?
|
||||
powerCapacityKva Float?
|
||||
waterSupplyM3Day Float?
|
||||
media Json?
|
||||
viewCount Int @default(0)
|
||||
inquiryCount Int @default(0)
|
||||
publishedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([parkId])
|
||||
@@index([propertyType])
|
||||
@@index([leaseType])
|
||||
@@index([status])
|
||||
@@index([areaM2])
|
||||
@@index([priceUsdM2])
|
||||
@@index([sellerId])
|
||||
@@index([agentId])
|
||||
@@index([publishedAt])
|
||||
@@index([parkId, status])
|
||||
@@index([propertyType, leaseType, status])
|
||||
@@index([status, publishedAt(sort: Desc)])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MESSAGING (buyer ↔ agent / seller in-app chat)
|
||||
// =============================================================================
|
||||
|
||||
enum ConversationStatus {
|
||||
ACTIVE
|
||||
ARCHIVED
|
||||
CLOSED
|
||||
}
|
||||
|
||||
model Conversation {
|
||||
id String @id @default(cuid())
|
||||
listingId String?
|
||||
subject String?
|
||||
status ConversationStatus @default(ACTIVE)
|
||||
lastMessage String? @db.Text
|
||||
lastMessageAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
participants ConversationParticipant[]
|
||||
messages Message[]
|
||||
|
||||
@@index([status])
|
||||
@@index([lastMessageAt(sort: Desc)])
|
||||
@@index([listingId])
|
||||
}
|
||||
|
||||
model ConversationParticipant {
|
||||
id String @id @default(cuid())
|
||||
conversationId String
|
||||
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
unreadCount Int @default(0)
|
||||
lastReadAt DateTime?
|
||||
joinedAt DateTime @default(now())
|
||||
|
||||
@@unique([conversationId, userId])
|
||||
@@index([userId])
|
||||
@@index([conversationId])
|
||||
}
|
||||
|
||||
enum MessageType {
|
||||
TEXT
|
||||
IMAGE
|
||||
FILE
|
||||
SYSTEM
|
||||
}
|
||||
|
||||
model Message {
|
||||
id String @id @default(cuid())
|
||||
conversationId String
|
||||
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
||||
senderId String
|
||||
type MessageType @default(TEXT)
|
||||
content String @db.Text
|
||||
metadata Json?
|
||||
editedAt DateTime?
|
||||
deletedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([conversationId, createdAt])
|
||||
@@index([senderId])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user