Add SMS templates, prepaid IRT billing, and advertising shortcode 9000590009.

Charge ceil(len/64)*400 IRT per recipient across OTP, customer, and partner SMS; seed new businesses with 500k IRT; default partner sends to the advertising line.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-08 16:24:15 +03:30
co-authored by Cursor
parent 4623061a4f
commit 4a1d60dbef
29 changed files with 1170 additions and 61 deletions
+18 -11
View File
@@ -28,6 +28,7 @@ import { UpdateProfileDto } from './dto/update-profile.dto';
import { PermissionsService } from './permissions.service';
import { parseUserProfile } from './profile.util';
import { SmsService } from './sms.service';
import { SmsBillingService } from './sms-billing.service';
const OTP_TTL_SECONDS = 300;
/** Pending cross-site password change (longer than OTP so resend still works). */
@@ -44,6 +45,7 @@ export class AuthService {
private readonly config: ConfigService,
private readonly redis: RedisService,
private readonly sms: SmsService,
private readonly smsBilling: SmsBillingService,
private readonly tenant: TenantService,
private readonly permissions: PermissionsService,
) {}
@@ -403,7 +405,7 @@ export class AuthService {
return { message: 'Password changed successfully' };
}
async sendOtp(cellNumber: string, domain?: string) {
async sendOtp(cellNumber: string, domain: string) {
if (!this.sms.isEnabled()) {
return {
enabled: false,
@@ -420,24 +422,29 @@ export class AuthService {
throw new UnauthorizedException('Cell number is not registered');
}
let businessNameFa: string | undefined;
const host = domain?.trim();
if (host) {
try {
const business = await this.tenant.resolveBusinessByDomain(host);
businessNameFa =
business.nameFa?.trim() || business.name?.trim() || undefined;
} catch {
// Domain may be unknown — still send OTP without branding suffix
}
if (!host) {
throw new BadRequestException(
'Domain is required to send SMS (prepaid balance is billed per business).',
);
}
const business = await this.tenant.resolveBusinessByDomain(host);
const businessNameFa =
business.nameFa?.trim() || business.name?.trim() || undefined;
const code = this.generateOtpCode();
const brand = businessNameFa?.trim();
const message = brand
? `کد تایید شما: ${code}\n${brand}`
: `کد تایید شما: ${code}`;
const charged = await this.smsBilling.charge(business.id, message);
await this.redis.setOtp(cellNumber, code, OTP_TTL_SECONDS);
try {
await this.sms.sendVerificationCode(cellNumber, code, businessNameFa);
await this.sms.sendMessage(cellNumber, message);
} catch (err) {
await this.smsBilling.refund(business.id, charged);
if (err instanceof HttpException) {
throw err;
}