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") } /// other = سایت‌های متفرقه: فقط ثبت مشخصات و پیگیری سرور/افزونه/پشتیبانی/پیامک؛ بدون کنترل ریموت و بدون get_user. enum ProductType { hamkar khanegi other } /// Khanegi front-end appearance family (ignored for hamkar). enum KhanegiUiVariant { modular newTheme } enum UserRole { super_admin admin } 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) /// Permission codes for role=admin (ignored when role=super_admin) permissions Json @default("[]") 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") /// Khanegi only: which appearance editor / get_user payload shape to use. uiVariant KhanegiUiVariant @default(modular) @map("ui_variant") /// 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") /// Kill switch: block this sold version's app+admin (server stays up). Not the same as ServerAccount billing. serverAccessBlocked Boolean @default(false) @map("server_access_blocked") serverAccessBlockedAt DateTime? @map("server_access_blocked_at") 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[] pluginPurchaseRequests PluginPurchaseRequest[] supportSubscriptions SupportSubscription[] settings Setting[] @@index([product]) @@index([isActive]) @@map("tenants") } /// قالب پیش‌فرض تنظیمات (از deploy_settings_const). نسخهٔ جدید از اینجا کپی می‌شود. model SettingTemplate { id String @id @default(cuid()) key String @unique @db.VarChar(50) titleFa String? @map("title_fa") @db.VarChar(255) canEdit Int? @default(0) @map("can_edit") @db.SmallInt value String? @db.Text description String? @db.Text type String? @db.VarChar(50) /// ستون همکار — برای سازگاری آینده typeData String? @map("type_data") @db.VarChar(255) isExist Boolean @default(true) @map("is_exist") isActive Int? @default(1) @map("is_active") @db.SmallInt createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") @@map("setting_templates") } /// تنظیمات هر نسخه — مثل جدول settings خانگی + tenant_id model Setting { id String @id @default(cuid()) tenantId String @map("tenant_id") tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) key String @db.VarChar(50) titleFa String? @map("title_fa") @db.VarChar(255) canEdit Int? @default(0) @map("can_edit") @db.SmallInt value String? @db.Text description String? @db.Text type String? @db.VarChar(50) /// ستون همکار — برای سازگاری آینده typeData String? @map("type_data") @db.VarChar(255) isExist Boolean @default(true) @map("is_exist") isActive Int? @default(1) @map("is_active") @db.SmallInt createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") @@unique([tenantId, key]) @@index([key]) @@index([tenantId]) @@map("settings") } /// 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") /// If set: block non-alert SMS when balanceRials < this. Null = only manual isBlocked. autoBlockBelowRials Int? @map("auto_block_below_rials") /// 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") /// Khanegi catalog key (jibit_enabled, …). Null = custom / hamkar. settingKey String? @map("setting_key") @db.VarChar(50) autoCutOnExpiry Boolean @default(false) @map("auto_cut_on_expiry") /// Sent expiry SMS marks: { "expiresAtIso": { "3": true, "2": true, "1": true, "0": true } } alertMarks Json? @map("alert_marks") 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]) @@index([settingKey]) @@map("plugin_subscriptions") } enum PluginPurchaseStatus { pending approved rejected cancelled } model PluginCatalogItem { id String @id @default(cuid()) settingKey String @unique @map("setting_key") @db.VarChar(50) titleFa String @map("title_fa") description String? @db.Text images Json? inStore Boolean @default(true) @map("in_store") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") prices PluginCatalogPrice[] requests PluginPurchaseRequest[] @@map("plugin_catalog_items") } model PluginCatalogPrice { id String @id @default(cuid()) catalogItemId String @map("catalog_item_id") durationDays Int @map("duration_days") enabled Boolean @default(false) originalPriceRials Int @default(0) @map("original_price_rials") finalPriceRials Int @default(0) @map("final_price_rials") catalogItem PluginCatalogItem @relation(fields: [catalogItemId], references: [id], onDelete: Cascade) @@unique([catalogItemId, durationDays]) @@map("plugin_catalog_prices") } model PluginPurchaseRequest { id String @id @default(cuid()) tenantId String @map("tenant_id") catalogItemId String @map("catalog_item_id") durationDays Int @map("duration_days") originalPriceRials Int @map("original_price_rials") finalPriceRials Int @map("final_price_rials") status PluginPurchaseStatus @default(pending) autoCutOnExpiry Boolean? @map("auto_cut_on_expiry") pluginSubscriptionId String? @map("plugin_subscription_id") receiptUrl String? @map("receipt_url") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") decidedAt DateTime? @map("decided_at") tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) catalogItem PluginCatalogItem @relation(fields: [catalogItemId], references: [id], onDelete: Restrict) @@index([tenantId, status]) @@index([catalogItemId]) @@index([createdAt]) @@map("plugin_purchase_requests") } 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") } /// Support/maintenance subscription for a sold version (0..N per tenant). /// Can be free (isFree) for 1|3|6|12 months gift periods. model SupportSubscription { id String @id @default(cuid()) tenantId String @map("tenant_id") name String specs String? priceRials Int? @map("price_rials") /// When true, current period is complimentary (price should be 0) isFree Boolean @default(false) @map("is_free") 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 SupportCharge[] @@index([tenantId]) @@index([expiresAt]) @@index([isActive]) @@index([isFree]) @@map("support_subscriptions") } model SupportCharge { id String @id @default(cuid()) supportId String @map("support_id") chargedAt DateTime @map("charged_at") planMonths Int @map("plan_months") priceRials Int @map("price_rials") isFree Boolean @default(false) @map("is_free") expiresAt DateTime @map("expires_at") note String? createdById String? @map("created_by_id") createdAt DateTime @default(now()) @map("created_at") support SupportSubscription @relation(fields: [supportId], references: [id], onDelete: Cascade) @@index([supportId]) @@index([createdAt]) @@map("support_charges") }