feat: add per-org SMS wallet with adm panel and header credit

Bill all platform SMS (including OTP), allow negative balance with manual/auto cutoffs, and support own-panel bypass without wallet debit.
This commit is contained in:
2026-10-19 15:55:48 +03:30
parent f15ebb72ea
commit f5d68d83fc
42 changed files with 2073 additions and 121 deletions
+73
View File
@@ -206,6 +206,7 @@ model Organization {
webhookEndpoints WebhookEndpoint[]
smsCampaigns SmsCampaign[]
outboundSmsLogs OutboundSmsLog[]
smsAccount SmsAccount?
financialAccounts FinancialAccount[]
financialCategories FinancialCategory[]
financialDocuments FinancialDocument[]
@@ -2290,6 +2291,78 @@ model FinancialInstallment {
@@index([dueDate])
}
/// کیف‌پول و تنظیمات پیامک هر محیط (الگوی همکار / سوپرادمین آبشُده)
model SmsAccount {
id String @id @default(cuid())
organizationId String @unique
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
balanceRials Int @default(0)
pricePerPartRials Int @default(2500)
isBlocked Boolean @default(false)
/// اگر ست باشد: وقتی موجودی < این مقدار، ارسال قطع می‌شود (می‌تواند منفی باشد). null = فقط کلید دستی
autoBlockBelowRials Int?
alertCriticalRials Int @default(10000)
alertWarningRials Int @default(100000)
/// platform = سامانهٔ ما | own = پنل اختصاصی مشتری (بدون کسر/لاگ کیف)
panelMode String @default("platform")
/// وقتی panelMode=own: 0098 | asanak | trez
ownPanelType String @default("0098")
ownPanelUsername String?
ownPanelPassword String?
ownPanelFrom String?
chargeCard String?
chargeIban String?
chargeHolder String?
chargeInstruction String?
alertDaysSent Int @default(0)
lastAlertDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ledgers SmsLedger[]
sendLogs SmsBillingSendLog[]
@@index([isBlocked])
@@index([balanceRials])
}
model SmsLedger {
id String @id @default(cuid())
smsAccountId String
smsAccount SmsAccount @relation(fields: [smsAccountId], references: [id], onDelete: Cascade)
/// charge | debit | alert_debit
type String
amountRials Int
balanceAfterRials Int
parts Int?
pricePerPartRials Int?
note String?
smsSendLogId String?
createdById String?
createdAt DateTime @default(now())
@@index([smsAccountId, createdAt])
@@index([type])
}
/// لاگ ارسال صورتحساب‌شده (همهٔ مسیرها وقتی panelMode=platform)
model SmsBillingSendLog {
id String @id @default(cuid())
smsAccountId String
smsAccount SmsAccount @relation(fields: [smsAccountId], references: [id], onDelete: Cascade)
receiver String
body String
parts Int
pricePerPartRials Int
totalAmountRials Int
provider String?
source String?
isAlert Boolean @default(false)
createdAt DateTime @default(now())
@@index([smsAccountId, createdAt])
@@index([source])
}
/// لاگ یکپارچهٔ پیامک خروجی (دستی / کمپین / اتوماسیون)
model OutboundSmsLog {
id String @id @default(cuid())