fix(sms): send form confirmations to the lead number
Lead-only website forms had no person mobile, so the confirmation was marked sent without a number. Use the lead phone and send the full text. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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<boolean> {
|
||||
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(
|
||||
|
||||
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user