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
@@ -2,14 +2,13 @@ import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
import { LoyaltyModule } from '../loyalty/loyalty.module';
import { NotificationsModule } from '../notifications/notifications.module';
import { SmsService } from '../common/sms.service';
import { AutomationsController } from './automations.controller';
import { AutomationsService } from './automations.service';
@Module({
imports: [AuthModule, NotificationsModule, LoyaltyModule],
controllers: [AutomationsController],
providers: [AutomationsService, SmsService],
providers: [AutomationsService],
exports: [AutomationsService],
})
export class AutomationsModule {}
@@ -32,7 +32,7 @@ import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { NotificationsService } from '../notifications/notifications.service';
import { LoyaltyService } from '../loyalty/loyalty.service';
import { SmsService } from '../common/sms.service';
import { SmsGatewayService } from '../sms-billing/sms-gateway.service';
import {
formatFormAnswers,
pickRoundRobin,
@@ -79,7 +79,7 @@ export class AutomationsService {
private readonly prisma: PrismaService,
private readonly notifications: NotificationsService,
private readonly loyalty: LoyaltyService,
private readonly sms: SmsService,
private readonly sms: SmsGatewayService,
) {}
async list(organizationId: string): Promise<AutomationRuleDto[]> {
@@ -537,7 +537,11 @@ export class AutomationsService {
select: { mobile: true, phone: true },
})
: null;
await this.sendAutomationSms(person?.mobile || person?.phone, smsText);
await this.sendAutomationSms(
ctx.organizationId,
person?.mobile || person?.phone,
smsText,
);
}
return;
}
@@ -570,11 +574,12 @@ export class AutomationsService {
},
select: { mobile: true },
});
await this.sendAutomationSms(user?.mobile, smsText);
await this.sendAutomationSms(ctx.organizationId, user?.mobile, smsText);
}
}
private async sendAutomationSms(
organizationId: string,
mobile: string | null | undefined,
text: string,
) {
@@ -582,11 +587,12 @@ export class AutomationsService {
this.logger.warn('Automation SMS skipped: no mobile');
return;
}
if (!this.sms.isConfigured()) {
this.logger.warn('Automation SMS skipped: SMS not configured');
return;
}
const result = await this.sms.sendSmsDetailed(mobile, text);
const result = await this.sms.send({
organizationId,
phone: mobile,
text,
source: 'automation',
});
if (!result.ok) {
this.logger.warn(`Automation SMS failed: ${result.reason ?? 'unknown'}`);
}