feat: scaffold monorepo with Turborepo + NestJS + Next.js
- Turborepo monorepo with pnpm workspaces - apps/api: NestJS 11.x with CQRS module - apps/web: Next.js 14 App Router + TailwindCSS - src/modules/shared: base entities, Result pattern, value objects - TypeScript 5.7+ strict mode, shared tsconfig base - Build pipeline: dev, build, lint, test, typecheck Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
455
prisma/schema.prisma
Normal file
455
prisma/schema.prisma
Normal file
@@ -0,0 +1,455 @@
|
||||
// =============================================================================
|
||||
// GoodGo Platform — Prisma Schema
|
||||
// PostgreSQL 16 + PostGIS
|
||||
// =============================================================================
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["postgresqlExtensions"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
extensions = [postgis]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AUTH
|
||||
// =============================================================================
|
||||
|
||||
enum UserRole {
|
||||
BUYER
|
||||
SELLER
|
||||
AGENT
|
||||
ADMIN
|
||||
}
|
||||
|
||||
enum KYCStatus {
|
||||
NONE
|
||||
PENDING
|
||||
VERIFIED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String? @unique
|
||||
phone String @unique
|
||||
passwordHash String?
|
||||
fullName String
|
||||
avatarUrl String?
|
||||
role UserRole @default(BUYER)
|
||||
kycStatus KYCStatus @default(NONE)
|
||||
kycData Json?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
agent Agent?
|
||||
listings Listing[]
|
||||
savedSearches SavedSearch[]
|
||||
subscription Subscription?
|
||||
payments Payment[]
|
||||
reviews Review[]
|
||||
inquiriesSent Inquiry[]
|
||||
|
||||
@@index([phone])
|
||||
@@index([role])
|
||||
}
|
||||
|
||||
model Agent {
|
||||
id String @id @default(cuid())
|
||||
userId String @unique
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
licenseNumber String?
|
||||
agency String?
|
||||
qualityScore Float @default(0)
|
||||
totalDeals Int @default(0)
|
||||
responseTimeAvg Int?
|
||||
bio String?
|
||||
serviceAreas Json // ["quan-1", "quan-7", "thu-duc"]
|
||||
isVerified Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
listings Listing[]
|
||||
leads Lead[]
|
||||
|
||||
@@index([qualityScore])
|
||||
@@index([isVerified])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LISTINGS
|
||||
// =============================================================================
|
||||
|
||||
enum PropertyType {
|
||||
APARTMENT
|
||||
VILLA
|
||||
TOWNHOUSE
|
||||
LAND
|
||||
OFFICE
|
||||
SHOPHOUSE
|
||||
}
|
||||
|
||||
enum TransactionType {
|
||||
SALE
|
||||
RENT
|
||||
}
|
||||
|
||||
enum ListingStatus {
|
||||
DRAFT
|
||||
PENDING_REVIEW
|
||||
ACTIVE
|
||||
RESERVED
|
||||
SOLD
|
||||
RENTED
|
||||
EXPIRED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
enum Direction {
|
||||
NORTH
|
||||
SOUTH
|
||||
EAST
|
||||
WEST
|
||||
NORTHEAST
|
||||
NORTHWEST
|
||||
SOUTHEAST
|
||||
SOUTHWEST
|
||||
}
|
||||
|
||||
model Property {
|
||||
id String @id @default(cuid())
|
||||
propertyType PropertyType
|
||||
title String
|
||||
description String @db.Text
|
||||
address String
|
||||
ward String
|
||||
district String
|
||||
city String
|
||||
location Unsupported("geometry(Point, 4326)")
|
||||
areaM2 Float
|
||||
usableAreaM2 Float?
|
||||
bedrooms Int?
|
||||
bathrooms Int?
|
||||
floors Int?
|
||||
floor Int?
|
||||
totalFloors Int?
|
||||
direction Direction?
|
||||
yearBuilt Int?
|
||||
legalStatus String?
|
||||
amenities Json?
|
||||
nearbyPOIs Json?
|
||||
metroDistanceM Float?
|
||||
projectName String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
listings Listing[]
|
||||
valuations Valuation[]
|
||||
media PropertyMedia[]
|
||||
|
||||
@@index([propertyType])
|
||||
@@index([district, city])
|
||||
@@index([location], type: Gist)
|
||||
}
|
||||
|
||||
model PropertyMedia {
|
||||
id String @id @default(cuid())
|
||||
propertyId String
|
||||
property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)
|
||||
url String
|
||||
type String // "image" | "video"
|
||||
order Int @default(0)
|
||||
caption String?
|
||||
aiTags Json?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([propertyId])
|
||||
}
|
||||
|
||||
model Listing {
|
||||
id String @id @default(cuid())
|
||||
propertyId String
|
||||
property Property @relation(fields: [propertyId], references: [id])
|
||||
agentId String?
|
||||
agent Agent? @relation(fields: [agentId], references: [id])
|
||||
sellerId String
|
||||
seller User @relation(fields: [sellerId], references: [id])
|
||||
transactionType TransactionType
|
||||
status ListingStatus @default(DRAFT)
|
||||
priceVND BigInt
|
||||
pricePerM2 Float?
|
||||
rentPriceMonthly BigInt?
|
||||
commissionPct Float? @default(2.0)
|
||||
aiPriceEstimate BigInt?
|
||||
aiConfidence Float?
|
||||
moderationScore Float?
|
||||
moderationNotes String?
|
||||
viewCount Int @default(0)
|
||||
saveCount Int @default(0)
|
||||
inquiryCount Int @default(0)
|
||||
featuredUntil DateTime?
|
||||
expiresAt DateTime?
|
||||
publishedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
transactions Transaction[]
|
||||
inquiries Inquiry[]
|
||||
|
||||
@@index([status])
|
||||
@@index([transactionType])
|
||||
@@index([priceVND])
|
||||
@@index([agentId])
|
||||
@@index([publishedAt])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SEARCH
|
||||
// =============================================================================
|
||||
|
||||
model SavedSearch {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
filters Json
|
||||
alertEnabled Boolean @default(true)
|
||||
lastAlertAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TRANSACTIONS
|
||||
// =============================================================================
|
||||
|
||||
enum TransactionStatus {
|
||||
INQUIRY
|
||||
VIEWING_SCHEDULED
|
||||
OFFER_MADE
|
||||
DEPOSIT_PAID
|
||||
CONTRACT_SIGNING
|
||||
COMPLETED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
model Transaction {
|
||||
id String @id @default(cuid())
|
||||
listingId String
|
||||
listing Listing @relation(fields: [listingId], references: [id])
|
||||
buyerId String
|
||||
status TransactionStatus @default(INQUIRY)
|
||||
agreedPrice BigInt?
|
||||
depositAmount BigInt?
|
||||
timeline Json?
|
||||
contractUrl String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
payments Payment[]
|
||||
|
||||
@@index([listingId])
|
||||
@@index([buyerId])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model Inquiry {
|
||||
id String @id @default(cuid())
|
||||
listingId String
|
||||
listing Listing @relation(fields: [listingId], references: [id])
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
message String @db.Text
|
||||
phone String?
|
||||
isRead Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([listingId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model Lead {
|
||||
id String @id @default(cuid())
|
||||
agentId String
|
||||
agent Agent @relation(fields: [agentId], references: [id])
|
||||
name String
|
||||
phone String
|
||||
email String?
|
||||
source String
|
||||
score Float?
|
||||
notes Json?
|
||||
status String @default("new")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([agentId])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PAYMENTS
|
||||
// =============================================================================
|
||||
|
||||
enum PaymentProvider {
|
||||
VNPAY
|
||||
MOMO
|
||||
ZALOPAY
|
||||
BANK_TRANSFER
|
||||
}
|
||||
|
||||
enum PaymentStatus {
|
||||
PENDING
|
||||
PROCESSING
|
||||
COMPLETED
|
||||
FAILED
|
||||
REFUNDED
|
||||
}
|
||||
|
||||
enum PaymentType {
|
||||
SUBSCRIPTION
|
||||
LISTING_FEE
|
||||
DEPOSIT
|
||||
FEATURED_LISTING
|
||||
}
|
||||
|
||||
model Payment {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
transactionId String?
|
||||
transaction Transaction? @relation(fields: [transactionId], references: [id])
|
||||
provider PaymentProvider
|
||||
type PaymentType
|
||||
amountVND BigInt
|
||||
status PaymentStatus @default(PENDING)
|
||||
providerTxId String?
|
||||
callbackData Json?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
@@index([status])
|
||||
@@index([providerTxId])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SUBSCRIPTIONS
|
||||
// =============================================================================
|
||||
|
||||
enum PlanTier {
|
||||
FREE
|
||||
AGENT_PRO
|
||||
INVESTOR
|
||||
ENTERPRISE
|
||||
}
|
||||
|
||||
enum SubscriptionStatus {
|
||||
ACTIVE
|
||||
PAST_DUE
|
||||
CANCELLED
|
||||
EXPIRED
|
||||
}
|
||||
|
||||
model Plan {
|
||||
id String @id @default(cuid())
|
||||
tier PlanTier @unique
|
||||
name String
|
||||
priceMonthlyVND BigInt
|
||||
priceYearlyVND BigInt
|
||||
maxListings Int?
|
||||
maxSavedSearches Int?
|
||||
features Json
|
||||
isActive Boolean @default(true)
|
||||
|
||||
subscriptions Subscription[]
|
||||
}
|
||||
|
||||
model Subscription {
|
||||
id String @id @default(cuid())
|
||||
userId String @unique
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
planId String
|
||||
plan Plan @relation(fields: [planId], references: [id])
|
||||
status SubscriptionStatus @default(ACTIVE)
|
||||
currentPeriodStart DateTime
|
||||
currentPeriodEnd DateTime
|
||||
cancelledAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
usageRecords UsageRecord[]
|
||||
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model UsageRecord {
|
||||
id String @id @default(cuid())
|
||||
subscriptionId String
|
||||
subscription Subscription @relation(fields: [subscriptionId], references: [id])
|
||||
metric String
|
||||
count Int
|
||||
periodStart DateTime
|
||||
periodEnd DateTime
|
||||
|
||||
@@index([subscriptionId, metric])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ANALYTICS
|
||||
// =============================================================================
|
||||
|
||||
model Valuation {
|
||||
id String @id @default(cuid())
|
||||
propertyId String
|
||||
property Property @relation(fields: [propertyId], references: [id])
|
||||
estimatedPrice BigInt
|
||||
confidence Float
|
||||
pricePerM2 Float
|
||||
comparables Json
|
||||
features Json
|
||||
modelVersion String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([propertyId])
|
||||
}
|
||||
|
||||
model MarketIndex {
|
||||
id String @id @default(cuid())
|
||||
district String
|
||||
city String
|
||||
propertyType PropertyType
|
||||
period String
|
||||
medianPrice BigInt
|
||||
avgPriceM2 Float
|
||||
totalListings Int
|
||||
daysOnMarket Float
|
||||
inventoryLevel Int
|
||||
absorptionRate Float?
|
||||
yoyChange Float?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([district, city, propertyType, period])
|
||||
@@index([city, period])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// REVIEWS
|
||||
// =============================================================================
|
||||
|
||||
model Review {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
targetType String
|
||||
targetId String
|
||||
rating Int
|
||||
comment String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([targetType, targetId])
|
||||
}
|
||||
Reference in New Issue
Block a user