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
+1 -2
View File
@@ -5,12 +5,11 @@ import { LeadStagesService } from './lead-stages.service';
import { LeadsController } from './leads.controller';
import { LeadsImportService } from './leads-import.service';
import { LeadsService } from './leads.service';
import { SmsService } from '../common/sms.service';
@Module({
imports: [AuthModule, BillingModule],
controllers: [LeadsController],
providers: [LeadsService, LeadStagesService, LeadsImportService, SmsService],
providers: [LeadsService, LeadStagesService, LeadsImportService],
exports: [LeadsService, LeadStagesService],
})
export class LeadsModule {}
+13 -6
View File
@@ -21,7 +21,7 @@ import type {
} from '@mento/shared';
import { FILTERED_IDS_MAX } from '@mento/shared';
import { PrismaService } from '../prisma/prisma.service';
import { SmsService } from '../common/sms.service';
import { SmsGatewayService } from '../sms-billing/sms-gateway.service';
import { renderSmsText } from '../common/sms-text';
import { OutboundSmsLogService } from '../outbound-sms/outbound-sms-log.service';
import {
@@ -87,7 +87,7 @@ export class LeadsService {
private readonly stages: LeadStagesService,
private readonly files: LocalFileStorage,
private readonly game: GamificationService,
private readonly sms: SmsService,
private readonly sms: SmsGatewayService,
private readonly outboundSms: OutboundSmsLogService,
) {}
@@ -1291,7 +1291,12 @@ export class LeadsService {
company: lead.contactName ? lead.title : '',
});
if (!message) throw new BadRequestException('متن پیامک خالی است.');
const result = await this.sms.sendSmsDetailed(mobile, message);
const result = await this.sms.send({
organizationId: actor.organizationId,
phone: mobile,
text: message,
source: 'manual',
});
await this.outboundSms.record({
organizationId: actor.organizationId,
leadId: id,
@@ -1304,9 +1309,11 @@ export class LeadsService {
});
if (!result.ok) {
throw new ServiceUnavailableException(
result.reason === 'SMS not configured'
? 'ارسال پیامک روی سرور فعال نیست.'
: 'پیامک ارسال نشد. کمی بعد دوباره تلاش کنید.',
result.blocked
? 'ارسال پیامک به‌خاطر محدودیت اعتبار مسدود است.'
: result.reason === 'SMS not configured'
? 'ارسال پیامک روی سرور فعال نیست.'
: 'پیامک ارسال نشد. کمی بعد دوباره تلاش کنید.',
);
}
await this.addEvent(actor, id, 'note', 'پیامک ارسال شد', message);