diff --git a/apps/api/src/automations/automations.service.ts b/apps/api/src/automations/automations.service.ts index d97779d..ebbfd36 100644 --- a/apps/api/src/automations/automations.service.ts +++ b/apps/api/src/automations/automations.service.ts @@ -578,7 +578,7 @@ export class AutomationsService { const channel: AutomationNotifyChannel = config.notifyChannel ?? 'app'; const wantApp = channel === 'app' || channel === 'both'; const wantSms = channel === 'sms' || channel === 'both'; - const smsText = (config.body?.trim() || title).slice(0, 160); + const smsText = (config.body?.trim() || title).trim(); const link = entityLink(ctx); if (config.notifyTarget === 'person') { @@ -592,9 +592,16 @@ export class AutomationsService { select: { mobile: true, phone: true }, }) : null; + const leadId = ctx.leadId || (ctx.entityType === 'lead' ? ctx.entityId : null); + const lead = !person?.mobile && !person?.phone && leadId + ? await this.prisma.lead.findFirst({ + where: { id: leadId, organizationId: ctx.organizationId }, + select: { mobile: true, phone: true }, + }) + : null; await this.sendAutomationSms( ctx.organizationId, - person?.mobile || person?.phone, + person?.mobile || person?.phone || lead?.mobile || lead?.phone || ctx.mobile, smsText, ); } diff --git a/apps/api/src/message-automations/__tests__/message-automation.check.ts b/apps/api/src/message-automations/__tests__/message-automation.check.ts index 394feda..7368eeb 100644 --- a/apps/api/src/message-automations/__tests__/message-automation.check.ts +++ b/apps/api/src/message-automations/__tests__/message-automation.check.ts @@ -56,7 +56,7 @@ function testTemplate() { }) === 'سلام علی 0912', 'name phone vars', ); - assert(clipSms('الف'.repeat(200)).length === 160, 'sms clip'); + assert(clipSms('الف'.repeat(200)).length === 200, 'sms is not clipped'); } testDelay(); diff --git a/apps/api/src/message-automations/message-automation.util.ts b/apps/api/src/message-automations/message-automation.util.ts index 20e54e7..4bbbd24 100644 --- a/apps/api/src/message-automations/message-automation.util.ts +++ b/apps/api/src/message-automations/message-automation.util.ts @@ -17,7 +17,20 @@ export const MESSAGE_AUTOMATION_TRIGGERS = [ ] as const; export const MAX_MESSAGE_REPEAT = 10; -export const MAX_MESSAGE_SMS = 160; + +/** شمارهٔ مشتری: اول شخص، و اگر نبود شمارهٔ خود سرنخ. */ +export function customerPhone( + person: { mobile?: string | null; phone?: string | null } | null | undefined, + lead: { mobile?: string | null; phone?: string | null } | null | undefined, +): string { + return ( + person?.mobile?.trim() || + person?.phone?.trim() || + lead?.mobile?.trim() || + lead?.phone?.trim() || + '' + ); +} export function delayMs(value: number, unit: MessageDelayUnit): number { const n = Math.max(0, Math.trunc(Number(value) || 0)); @@ -92,10 +105,9 @@ export function applyMessageTemplate( }); } +/** متن کامل پیامک؛ بخش‌بندی و هزینه در درگاه حساب می‌شود. */ export function clipSms(text: string): string { - const trimmed = text.trim(); - if (trimmed.length <= MAX_MESSAGE_SMS) return trimmed; - return `${trimmed.slice(0, MAX_MESSAGE_SMS - 1)}…`; + return text.trim(); } /** When this event happens, cancel leftover messages of these earlier triggers. */ diff --git a/apps/api/src/message-automations/message-automations.service.ts b/apps/api/src/message-automations/message-automations.service.ts index 00f58e2..4b4f0b3 100644 --- a/apps/api/src/message-automations/message-automations.service.ts +++ b/apps/api/src/message-automations/message-automations.service.ts @@ -21,6 +21,7 @@ import { buildSendAts, cancelTriggersFor, clipSms, + customerPhone, } from './message-automation.util'; export type MessageAutomationContext = { @@ -354,13 +355,15 @@ export class MessageAutomationsService { }, }) : null; + const lead = await this.lookupLead(job); const personName = person ? [person.firstName, person.lastName].filter(Boolean).join(' ').trim() : ''; + const phone = customerPhone(person, lead); const vars = { title: job.entityTitle, - name: personName || job.entityTitle, - phone: person?.mobile || person?.phone || '', + name: personName || lead?.contactName?.trim() || lead?.title || job.entityTitle, + phone, date: formatJalaliDayFa(new Date()), company: person?.company?.name ?? '', }; @@ -405,26 +408,59 @@ export class MessageAutomationsService { } if (targetPerson && wantSms) { - await this.sendSms(person?.mobile || person?.phone, smsText, { + if (!phone) { + throw new Error('شماره مشتری یا سرنخ برای پیامک پیدا نشد'); + } + const sent = await this.sendSms(phone, smsText, { organizationId: job.organizationId, personId: job.personId, + leadId: lead?.id ?? null, ruleId: job.ruleId || job.rule.id || '', }); + if (!sent) { + throw new Error('ارسال پیامک به مشتری یا سرنخ ناموفق بود'); + } } } + /** سرنخ همان پرونده، یا سرنخی که به این شخص تبدیل شده. */ + private async lookupLead(job: { + organizationId: string; + entityId: string; + personId: string | null; + }) { + return this.prisma.lead.findFirst({ + where: { + organizationId: job.organizationId, + OR: [ + { id: job.entityId }, + ...(job.personId ? [{ convertedPersonId: job.personId }] : []), + ], + }, + select: { + id: true, + mobile: true, + phone: true, + contactName: true, + title: true, + }, + orderBy: { createdAt: 'desc' }, + }); + } + private async sendSms( mobile: string | null | undefined, text: string, log: { organizationId: string; personId?: string | null; + leadId?: string | null; ruleId: string; }, - ) { - if (!mobile) { + ): Promise { + if (!mobile?.trim()) { this.logger.warn('Message automation SMS skipped: no mobile'); - return; + return false; } const result = await this.sms.send({ organizationId: log.organizationId, @@ -432,10 +468,11 @@ export class MessageAutomationsService { text, source: 'automation', }); - if (log.personId) { + if (log.personId || log.leadId) { await this.outboundSms.record({ organizationId: log.organizationId, personId: log.personId, + leadId: log.leadId, mobile, body: text, source: 'automation', @@ -448,7 +485,9 @@ export class MessageAutomationsService { this.logger.warn( `Message automation SMS failed: ${result.reason ?? 'unknown'}`, ); + return false; } + return true; } private toData( diff --git a/apps/api/test/message-automation.test.ts b/apps/api/test/message-automation.test.ts index 5330437..a7dcb96 100644 --- a/apps/api/test/message-automation.test.ts +++ b/apps/api/test/message-automation.test.ts @@ -2,12 +2,12 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { MAX_MESSAGE_REPEAT, - MAX_MESSAGE_SMS, addDelay, applyMessageTemplate, buildSendAts, cancelTriggersFor, clipSms, + customerPhone, delayMs, } from '../src/message-automations/message-automation.util'; @@ -40,14 +40,28 @@ describe('اتوماسیون پیام', () => { assert.equal(applyMessageTemplate('{ name }', { name: 'x' }), 'x'); }); - it('کوتاه کردن پیامک', () => { + it('متن پیامک بریده نمی‌شود', () => { const long = 'ا'.repeat(200); - const clipped = clipSms(long); - assert.equal(clipped.length, MAX_MESSAGE_SMS); - assert.ok(clipped.endsWith('…')); + assert.equal(clipSms(long), long); assert.equal(clipSms(' کوتاه '), 'کوتاه'); }); + it('شماره سرنخ جای شماره مشتری می‌نشیند', () => { + assert.equal( + customerPhone(null, { mobile: '09120000000' }), + '09120000000', + ); + assert.equal( + customerPhone({ mobile: '', phone: null }, { mobile: '09121111111', phone: '021' }), + '09121111111', + ); + assert.equal( + customerPhone({ mobile: '09123333333' }, { mobile: '09124444444' }), + '09123333333', + ); + assert.equal(customerPhone(null, null), ''); + }); + it('لغو پیام‌های قبلی', () => { assert.deepEqual(cancelTriggersFor('invoice_paid'), ['invoice_issued']); assert.deepEqual(cancelTriggersFor('deal_won'), ['deal_created', 'deal_stage_changed']);