Drop unique tenant_id on server_accounts and add create/update/delete/renew APIs per server account, with list/expiring using the worst status. Co-authored-by: Cursor <cursoragent@cursor.com>
282 lines
10 KiB
Plaintext
282 lines
10 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
// Production + shared host: PostgreSQL. Local: Docker Postgres or same server DB.
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
enum ProductType {
|
|
hamkar
|
|
khanegi
|
|
}
|
|
|
|
enum UserRole {
|
|
super_admin
|
|
viewer
|
|
}
|
|
|
|
enum SmsLedgerType {
|
|
charge
|
|
debit
|
|
adjust
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
mobile String @unique
|
|
firstName String @map("first_name")
|
|
lastName String @map("last_name")
|
|
/// Optional legacy/contact email — login uses mobile
|
|
email String? @unique
|
|
passwordHash String @map("password_hash")
|
|
role UserRole @default(super_admin)
|
|
isActive Boolean @default(true) @map("is_active")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
auditLogs AuditLog[]
|
|
|
|
@@map("users")
|
|
}
|
|
|
|
model Tenant {
|
|
id String @id @default(cuid())
|
|
product ProductType
|
|
slug String @unique
|
|
nameFa String @map("name_fa")
|
|
nameEn String? @map("name_en")
|
|
domain String @unique
|
|
domains Json?
|
|
avatarUrl String? @map("avatar_url")
|
|
primaryColor String? @map("primary_color")
|
|
secondaryColor String? @map("secondary_color")
|
|
backgroundColor String? @map("background_color")
|
|
nodeServer String @map("node_server")
|
|
nodeWs String? @map("node_ws")
|
|
appUrl String? @map("app_url")
|
|
adminUrl String? @map("admin_url")
|
|
remoteApiToken String? @map("remote_api_token")
|
|
/// Legacy cache from older sync attempts — SuperAdmin SMS account is now source of truth
|
|
smsSettingsCache Json? @map("sms_settings_cache")
|
|
smsSyncedAt DateTime? @map("sms_synced_at")
|
|
/// Full Khanegi UI JSON (theme, navigation, pages). Hamkar: unused for now.
|
|
appUiConfig Json? @map("app_ui_config")
|
|
/// Client cache-bust / force-reload signal for this sold version (GET /get_version).
|
|
appVersion String @default("1") @map("app_version")
|
|
isActive Boolean @default(true) @map("is_active")
|
|
notes String?
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
auditLogs AuditLog[]
|
|
smsAccount SmsAccount?
|
|
/// 0..N hosting/billing servers per sold version (not the same as nodeServer API URL)
|
|
serverAccounts ServerAccount[]
|
|
pluginSubscriptions PluginSubscription[]
|
|
|
|
@@index([product])
|
|
@@index([isActive])
|
|
@@map("tenants")
|
|
}
|
|
|
|
/// SuperAdmin-owned SMS wallet + gateway config per sold version.
|
|
/// Does NOT replace Hamkar/Khanegi internal SMS panels yet.
|
|
model SmsAccount {
|
|
id String @id @default(cuid())
|
|
tenantId String @unique @map("tenant_id")
|
|
balanceRials Int @default(0) @map("balance_rials")
|
|
pricePerPartRials Int @default(2500) @map("price_per_part_rials")
|
|
isBlocked Boolean @default(false) @map("is_blocked")
|
|
/// below this => red/critical
|
|
alertCriticalRials Int @default(10000) @map("alert_critical_rials")
|
|
/// at/below this (and above critical) => yellow; above => green
|
|
alertWarningRials Int @default(100000) @map("alert_warning_rials")
|
|
gatewayProvider String? @map("gateway_provider")
|
|
melipayamakUser String? @map("melipayamak_user")
|
|
melipayamakPassword String? @map("melipayamak_password")
|
|
melipayamakFrom String? @map("melipayamak_from")
|
|
panelUsername String? @map("panel_username")
|
|
panelPassword String? @map("panel_password")
|
|
panelFrom String? @map("panel_from")
|
|
/// Deposit instructions shown to tenant managers (parity with Hamkar sms-panel)
|
|
chargeCard String? @map("charge_card")
|
|
chargeIban String? @map("charge_iban")
|
|
chargeHolder String? @map("charge_holder")
|
|
chargeInstruction String? @map("charge_instruction")
|
|
extraSettings Json? @map("extra_settings")
|
|
todayMessages Int @default(0) @map("today_messages")
|
|
todayParts Int @default(0) @map("today_parts")
|
|
todayCostRials Int @default(0) @map("today_cost_rials")
|
|
monthMessages Int @default(0) @map("month_messages")
|
|
monthParts Int @default(0) @map("month_parts")
|
|
monthCostRials Int @default(0) @map("month_cost_rials")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
ledgers SmsLedger[]
|
|
sendLogs SmsSendLog[]
|
|
|
|
@@map("sms_accounts")
|
|
}
|
|
|
|
model SmsLedger {
|
|
id String @id @default(cuid())
|
|
smsAccountId String @map("sms_account_id")
|
|
type SmsLedgerType
|
|
amountRials Int @map("amount_rials")
|
|
balanceAfterRials Int? @map("balance_after_rials")
|
|
parts Int?
|
|
note String?
|
|
createdById String? @map("created_by_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
smsAccount SmsAccount @relation(fields: [smsAccountId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([smsAccountId])
|
|
@@index([createdAt])
|
|
@@index([type])
|
|
@@map("sms_ledgers")
|
|
}
|
|
|
|
/// Per-tenant SMS send history (filled when products hook consume; empty until then)
|
|
model SmsSendLog {
|
|
id String @id @default(cuid())
|
|
smsAccountId String @map("sms_account_id")
|
|
receiver String
|
|
body String
|
|
parts Int @default(1)
|
|
pricePerPartRials Int @map("price_per_part_rials")
|
|
totalAmountRials Int @map("total_amount_rials")
|
|
provider String?
|
|
source String?
|
|
isAlert Boolean @default(false) @map("is_alert")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
smsAccount SmsAccount @relation(fields: [smsAccountId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([smsAccountId])
|
|
@@index([createdAt])
|
|
@@index([provider])
|
|
@@map("sms_send_logs")
|
|
}
|
|
|
|
model AuditLog {
|
|
id String @id @default(cuid())
|
|
action String
|
|
entityType String @map("entity_type")
|
|
entityId String? @map("entity_id")
|
|
tenantId String? @map("tenant_id")
|
|
userId String? @map("user_id")
|
|
before Json?
|
|
after Json?
|
|
meta Json?
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: SetNull)
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([tenantId])
|
|
@@index([userId])
|
|
@@index([createdAt])
|
|
@@map("audit_logs")
|
|
}
|
|
|
|
/// Server hosting status for a sold version.
|
|
/// isDedicated=true → customer-owned; SuperAdmin only tracks that flag.
|
|
/// isDedicated=false → we manage charge/renewal/expiry alerts.
|
|
model ServerAccount {
|
|
id String @id @default(cuid())
|
|
tenantId String @map("tenant_id")
|
|
isDedicated Boolean @default(false) @map("is_dedicated")
|
|
serverName String? @map("server_name")
|
|
specs String?
|
|
/// Price of the current/last plan period (rials)
|
|
priceRials Int? @map("price_rials")
|
|
/// Start of current paid period
|
|
chargedAt DateTime? @map("charged_at")
|
|
/// Plan length in months: 1 | 3 | 6 | 12
|
|
planMonths Int? @map("plan_months")
|
|
expiresAt DateTime? @map("expires_at")
|
|
description String?
|
|
/// Days before expiry to start SMS alerts (default 3)
|
|
alertDaysBefore Int @default(3) @map("alert_days_before")
|
|
/// Last calendar day an expiry alert was sent (avoid multi-send same day)
|
|
lastAlertDate DateTime? @map("last_alert_date")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
charges ServerCharge[]
|
|
|
|
@@index([tenantId])
|
|
@@index([expiresAt])
|
|
@@index([isDedicated])
|
|
@@map("server_accounts")
|
|
}
|
|
|
|
model ServerCharge {
|
|
id String @id @default(cuid())
|
|
serverAccountId String @map("server_account_id")
|
|
chargedAt DateTime @map("charged_at")
|
|
planMonths Int @map("plan_months")
|
|
priceRials Int @map("price_rials")
|
|
expiresAt DateTime @map("expires_at")
|
|
note String?
|
|
createdById String? @map("created_by_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
serverAccount ServerAccount @relation(fields: [serverAccountId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([serverAccountId])
|
|
@@index([createdAt])
|
|
@@map("server_charges")
|
|
}
|
|
|
|
/// Paid plugin/addon subscription for a sold version (0..N per tenant).
|
|
model PluginSubscription {
|
|
id String @id @default(cuid())
|
|
tenantId String @map("tenant_id")
|
|
/// Plugin display name (required; each tenant can have many)
|
|
name String
|
|
specs String?
|
|
priceRials Int? @map("price_rials")
|
|
chargedAt DateTime? @map("charged_at")
|
|
/// Plan length in months: 1 | 3 | 6 | 12
|
|
planMonths Int? @map("plan_months")
|
|
expiresAt DateTime? @map("expires_at")
|
|
description String?
|
|
alertDaysBefore Int @default(3) @map("alert_days_before")
|
|
lastAlertDate DateTime? @map("last_alert_date")
|
|
isActive Boolean @default(true) @map("is_active")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
charges PluginCharge[]
|
|
|
|
@@index([tenantId])
|
|
@@index([expiresAt])
|
|
@@index([isActive])
|
|
@@map("plugin_subscriptions")
|
|
}
|
|
|
|
model PluginCharge {
|
|
id String @id @default(cuid())
|
|
pluginId String @map("plugin_id")
|
|
chargedAt DateTime @map("charged_at")
|
|
planMonths Int @map("plan_months")
|
|
priceRials Int @map("price_rials")
|
|
expiresAt DateTime @map("expires_at")
|
|
note String?
|
|
createdById String? @map("created_by_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
plugin PluginSubscription @relation(fields: [pluginId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([pluginId])
|
|
@@index([createdAt])
|
|
@@map("plugin_charges")
|
|
}
|