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
+7 -6
View File
@@ -22,7 +22,7 @@ import { Prisma } from '@prisma/client';
/** Ensures Express.Multer.File augmentation is loaded for saveAvatar. */
import type {} from 'multer';
import { PrismaService } from '../prisma/prisma.service';
import { SmsService } from '../common/sms.service';
import { SmsGatewayService } from '../sms-billing/sms-gateway.service';
import { LocalFileStorage } from '../storage/local-file.storage';
import { MembershipsService } from '../memberships/memberships.service';
import { BillingService } from '../billing/billing.service';
@@ -107,7 +107,7 @@ export class AuthService {
private readonly prisma: PrismaService,
private readonly jwt: JwtService,
private readonly config: ConfigService,
private readonly sms: SmsService,
private readonly sms: SmsGatewayService,
private readonly files: LocalFileStorage,
private readonly memberships: MembershipsService,
private readonly billing: BillingService,
@@ -174,7 +174,7 @@ export class AuthService {
if (emailOtpEnabled && emailCode) {
await this.dispatchEmailOtp(email, emailCode, returnOtp);
}
await this.dispatchMobileOtp(mobile, mobileCode, returnOtp);
await this.dispatchMobileOtp(mobile, mobileCode, returnOtp, null);
return {
challengeId: challenge.id,
@@ -353,7 +353,7 @@ export class AuthService {
},
});
await this.dispatchMobileOtp(mobile, code, returnOtp);
await this.dispatchMobileOtp(mobile, code, returnOtp, user.organizationId);
return {
challengeId: challenge.id,
@@ -875,9 +875,10 @@ export class AuthService {
mobile: string,
code: string,
returnOtp: boolean,
organizationId: string | null,
) {
if (this.sms.isConfigured()) {
const sent = await this.sms.sendOtpSms(mobile, code);
if (this.sms.isConfigured() || organizationId) {
const sent = await this.sms.sendOtp(organizationId, mobile, code);
if (!sent && !returnOtp) {
throw new BadRequestException(
'ارسال پیامک ناموفق بود؛ کمی بعد دوباره تلاش کنید',