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,14 +1,13 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
import { NotificationsModule } from '../notifications/notifications.module';
import { SmsService } from '../common/sms.service';
import { MessageAutomationsController } from './message-automations.controller';
import { MessageAutomationsService } from './message-automations.service';
@Module({
imports: [AuthModule, NotificationsModule],
controllers: [MessageAutomationsController],
providers: [MessageAutomationsService, SmsService],
providers: [MessageAutomationsService],
exports: [MessageAutomationsService],
})
export class MessageAutomationsModule {}
@@ -13,7 +13,7 @@ import type {
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { NotificationsService } from '../notifications/notifications.service';
import { SmsService } from '../common/sms.service';
import { SmsGatewayService } from '../sms-billing/sms-gateway.service';
import { dateToJalali, jalaliMonthName } from '../common/jalali';
import { OutboundSmsLogService } from '../outbound-sms/outbound-sms-log.service';
import {
@@ -47,7 +47,7 @@ export class MessageAutomationsService {
constructor(
private readonly prisma: PrismaService,
private readonly notifications: NotificationsService,
private readonly sms: SmsService,
private readonly sms: SmsGatewayService,
private readonly outboundSms: OutboundSmsLogService,
) {}
@@ -397,7 +397,10 @@ export class MessageAutomationsService {
},
select: { mobile: true },
});
await this.sendSms(user?.mobile, smsText);
await this.sendSms(user?.mobile, smsText, {
organizationId: job.organizationId,
ruleId: job.ruleId || job.rule.id || '',
});
}
}
@@ -413,7 +416,7 @@ export class MessageAutomationsService {
private async sendSms(
mobile: string | null | undefined,
text: string,
log?: {
log: {
organizationId: string;
personId?: string | null;
ruleId: string;
@@ -423,12 +426,13 @@ export class MessageAutomationsService {
this.logger.warn('Message automation SMS skipped: no mobile');
return;
}
if (!this.sms.isConfigured()) {
this.logger.warn('Message automation SMS skipped: SMS not configured');
return;
}
const result = await this.sms.sendSmsDetailed(mobile, text);
if (log?.personId) {
const result = await this.sms.send({
organizationId: log.organizationId,
phone: mobile,
text,
source: 'automation',
});
if (log.personId) {
await this.outboundSms.record({
organizationId: log.organizationId,
personId: log.personId,