feat(plugins): sell khanegi plugins as priced subscriptions

Catalog prices, purchase requests, and setting-key activation live on SuperAdmin so module admins can buy add-ons. Also rewrite legacy nerkh logo hosts to the SuperAdmin upload origin.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-07 16:56:51 +03:30
co-authored by Cursor
parent ccf7e90922
commit 1f776cf6e2
16 changed files with 1386 additions and 14 deletions
+66
View File
@@ -87,6 +87,7 @@ model Tenant {
/// 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[]
@@ -311,6 +312,11 @@ model PluginSubscription {
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")
@@ -321,9 +327,69 @@ model PluginSubscription {
@@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
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")
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")