Files
goodgo-platform/prisma/schema.prisma
Ho Ngoc Hai 91b76d567b fix(api): add JWT scheme to @ApiBearerAuth and fix Prisma 7 extensions config
- Add 'JWT' scheme name to @ApiBearerAuth() in payments & subscriptions
  controllers so Swagger UI correctly links to the JWT security definition
- Add postgresqlExtensions preview feature to Prisma schema for v7 compat

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 13:08:03 +07:00

550 lines
13 KiB
Plaintext

// =============================================================================
// GoodGo Platform — Prisma Schema
// PostgreSQL 16 + PostGIS
// =============================================================================
generator client {
provider = "prisma-client-js"
previewFeatures = ["postgresqlExtensions"]
}
datasource db {
provider = "postgresql"
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[]
refreshTokens RefreshToken[]
oauthAccounts OAuthAccount[]
@@index([phone])
@@index([role])
}
enum OAuthProvider {
GOOGLE
ZALO
}
model RefreshToken {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
token String @unique
family String
expiresAt DateTime
revokedAt DateTime?
createdAt DateTime @default(now())
@@index([userId])
@@index([family])
@@index([expiresAt])
}
model OAuthAccount {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
provider OAuthProvider
providerUserId String
accessToken String?
refreshToken String?
expiresAt DateTime?
profile Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([provider, providerUserId])
@@index([userId])
}
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([sellerId])
@@index([propertyId])
@@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?
idempotencyKey String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, provider, idempotencyKey], name: "Payment_idempotency_unique")
@@index([userId])
@@index([transactionId])
@@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([planId])
@@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])
}
// =============================================================================
// NOTIFICATIONS
// =============================================================================
enum NotificationChannel {
EMAIL
SMS
PUSH
ZALO_OA
}
enum NotificationStatus {
PENDING
SENT
FAILED
DELIVERED
}
model NotificationLog {
id String @id @default(cuid())
userId String
channel NotificationChannel
templateKey String
subject String?
body String @db.Text
metadata Json?
status NotificationStatus @default(PENDING)
errorDetail String?
sentAt DateTime?
createdAt DateTime @default(now())
@@index([userId])
@@index([channel, status])
@@index([templateKey])
@@index([createdAt])
}
model NotificationPreference {
id String @id @default(cuid())
userId String
channel NotificationChannel
eventType String
enabled Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, channel, eventType])
@@index([userId])
}
// =============================================================================
// 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])
}