Add production-ready SuperAdmin API features for tenants, SMS, servers, and plugins.

Includes mobile auth, get_user/get_version, SMS wallet parity endpoints, appearance backfill, and related Prisma migrations.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-30 18:19:53 +03:30
co-authored by Cursor
parent ddf36f8470
commit a6b3457779
54 changed files with 3770 additions and 156 deletions
+145 -12
View File
@@ -26,9 +26,12 @@ enum SmsLedgerType {
model User {
id String @id @default(cuid())
email String @unique
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")
name String
role UserRole @default(super_admin)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
@@ -60,12 +63,16 @@ model Tenant {
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?
serverAccount ServerAccount?
pluginSubscriptions PluginSubscription[]
@@index([product])
@@index([isActive])
@@ -91,6 +98,11 @@ model SmsAccount {
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")
@@ -101,29 +113,54 @@ model SmsAccount {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
ledgers SmsLedger[]
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")
parts Int?
note String?
createdById String? @map("created_by_id")
createdAt DateTime @default(now()) @map("created_at")
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
@@ -144,3 +181,99 @@ model AuditLog {
@@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 @unique @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([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")
}