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
@@ -1,6 +1,5 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
import { SmsService } from '../common/sms.service';
import { PortalController } from './portal.controller';
import { PortalService } from './portal.service';
import { PortalAuthGuard } from './portal-auth';
@@ -8,6 +7,6 @@ import { PortalAuthGuard } from './portal-auth';
@Module({
imports: [AuthModule],
controllers: [PortalController],
providers: [PortalService, PortalAuthGuard, SmsService],
providers: [PortalService, PortalAuthGuard],
})
export class PortalModule {}
+9 -12
View File
@@ -19,7 +19,7 @@ import type {
} from '@mento/shared';
import { TICKET_SLA_HOURS } from '@mento/shared';
import { PrismaService } from '../prisma/prisma.service';
import { SmsService } from '../common/sms.service';
import { SmsGatewayService } from '../sms-billing/sms-gateway.service';
import type { PortalActor } from './portal-auth';
const OTP_TTL_SECONDS = 5 * 60;
@@ -31,7 +31,7 @@ export class PortalService {
private readonly prisma: PrismaService,
private readonly jwt: JwtService,
private readonly config: ConfigService,
private readonly sms: SmsService,
private readonly sms: SmsGatewayService,
) {}
async getOrgInfo(slug: string): Promise<PortalOrgInfoDto> {
@@ -99,16 +99,13 @@ export class PortalService {
(this.config.get<string>('NODE_ENV') !== 'production' &&
this.config.get<string>('PORTAL_SHOW_OTP') !== 'false');
if (this.sms.isConfigured()) {
const sent = await this.sms.sendOtpSms(mobile, code);
if (!sent && !returnOtp) {
throw new BadRequestException(
'ارسال پیامک ناموفق بود؛ کمی بعد دوباره تلاش کنید',
);
}
} else if (!returnOtp) {
throw new BadRequestException('سامانه پیامک پیکربندی نشده است');
} else {
const sent = await this.sms.sendOtp(org.id, mobile, code);
if (!sent && !returnOtp) {
throw new BadRequestException(
'ارسال پیامک ناموفق بود؛ کمی بعد دوباره تلاش کنید',
);
}
if (!sent) {
// eslint-disable-next-line no-console
console.log(`[portal-otp] ${org.slug} ${mobile} => ${code}`);
}