feat: send one SMS from a person or lead and keep the dashboard inside the phone frame

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-13 21:05:17 +03:30
co-authored by Cursor
parent 13ee70764b
commit 42f0573a5f
13 changed files with 733 additions and 113 deletions
+32
View File
@@ -3,6 +3,7 @@ import {
ConflictException,
Injectable,
NotFoundException,
ServiceUnavailableException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { parseBirthDate } from '../common/excel';
@@ -18,6 +19,8 @@ import type {
PaginatedResponse,
} from '@mento/shared';
import { PrismaService } from '../prisma/prisma.service';
import { SmsService } from '../common/sms.service';
import { renderSmsText } from '../common/sms-text';
import {
type Actor,
elevated,
@@ -81,6 +84,7 @@ export class LeadsService {
private readonly stages: LeadStagesService,
private readonly files: LocalFileStorage,
private readonly game: GamificationService,
private readonly sms: SmsService,
) {}
private gameEvent(
@@ -1203,6 +1207,34 @@ export class LeadsService {
return this.toDto(row);
}
async sendSms(actor: Actor, id: string, text: string) {
const body = text.trim();
if (!body || body.length > 500) {
throw new BadRequestException('متن پیامک را تا ۵۰۰ نویسه بنویسید.');
}
const lead = await this.findAccessible(actor, id);
const mobile = lead.mobile || lead.phone;
if (!mobile) throw new BadRequestException('برای این لید شماره موبایل ثبت نشده است.');
const name = (lead.contactName || lead.title).trim();
const message = renderSmsText(body, {
name,
phone: mobile,
title: lead.title,
company: lead.contactName ? lead.title : '',
});
if (!message) throw new BadRequestException('متن پیامک خالی است.');
const result = await this.sms.sendSmsDetailed(mobile, message);
if (!result.ok) {
throw new ServiceUnavailableException(
result.reason === 'SMS not configured'
? 'ارسال پیامک روی سرور فعال نیست.'
: 'پیامک ارسال نشد. کمی بعد دوباره تلاش کنید.',
);
}
await this.addEvent(actor, id, 'note', 'پیامک ارسال شد', message);
return { ok: true };
}
async openLogo(actor: Actor, id: string) {
const existing = await this.findAccessible(actor, id);
if (!existing.logoStorageKey) {