Fix error sending reminder SMS

This commit is contained in:
2026-10-06 13:49:03 +03:30
parent 5074c6d791
commit bc01384551
6 changed files with 191 additions and 48 deletions
@@ -1,9 +1,27 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { brand } from '@mento/shared';
import type { NotificationDto, NotificationsListDto } from '@mento/shared';
import { PrismaService } from '../prisma/prisma.service';
import { SmsService } from '../common/sms.service';
/** یادآورهایی که بیش از این دیرتر پردازش شوند (مثلاً قطعی سرور) فقط اعلان می‌گیرند، نه پیامک. */
const REMINDER_SMS_MAX_DELAY_MS = 6 * 60 * 60 * 1000;
const TEHRAN_DATE_TIME = new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
timeZone: 'Asia/Tehran',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
function formatTehranDateTime(d: Date): string {
return TEHRAN_DATE_TIME.format(d);
}
export type CreateNotificationInput = {
organizationId: string;
userId: string;
@@ -143,83 +161,142 @@ export class NotificationsService {
});
}
const dueActivities = await this.prisma.activity.findMany({
}
/** یادآورهای فعالیت و یادداشت را سر وقت (مستقل از آنلاین بودن کاربر) ارسال می‌کند. */
@Cron('* * * * *')
async processDueReminders(): Promise<void> {
if (this.reminderRunning) return;
this.reminderRunning = true;
try {
await this.processActivityReminders();
await this.processNoteReminders();
} catch (err) {
this.logger.warn(
`reminder run failed: ${err instanceof Error ? err.message : 'unknown'}`,
);
} finally {
this.reminderRunning = false;
}
}
private reminderRunning = false;
private async processActivityReminders(): Promise<void> {
const now = new Date();
const due = await this.prisma.activity.findMany({
where: {
organizationId,
assigneeId: userId,
status: 'open',
reminderAt: { lte: now },
reminderSentAt: null,
},
select: {
id: true,
organizationId: true,
title: true,
dueAt: true,
reminderAt: true,
reminderNotifyApp: true,
reminderNotifySms: true,
assignee: { select: { mobile: true, fullName: true } },
createdById: true,
person: { select: { firstName: true, lastName: true, mobile: true } },
company: { select: { name: true } },
assignee: { select: { id: true, mobile: true } },
assignees: { select: { id: true, mobile: true } },
},
take: 20,
orderBy: { reminderAt: 'asc' },
take: 200,
});
for (const activity of dueActivities) {
if (activity.reminderNotifyApp) {
await this.create({
organizationId,
userId,
type: 'activity_reminder',
title: 'یادآوری فعالیت',
body: activity.title,
link: `/activities/${activity.id}`,
entityType: 'activity',
entityId: activity.id,
dedupe: true,
});
}
if (activity.reminderNotifySms) {
await this.sendActivityReminderSms(
activity.assignee?.mobile,
activity.title,
);
}
await this.prisma.activity.update({
where: { id: activity.id },
data: { reminderAt: null },
for (const activity of due) {
const claimed = await this.prisma.activity.updateMany({
where: { id: activity.id, reminderSentAt: null, reminderAt: activity.reminderAt },
data: { reminderSentAt: new Date() },
});
}
if (!claimed.count) continue;
const recipients = new Map<string, string | null>();
if (activity.assignee) recipients.set(activity.assignee.id, activity.assignee.mobile);
for (const u of activity.assignees) recipients.set(u.id, u.mobile);
if (!recipients.size && activity.createdById) {
const creator = await this.prisma.user.findUnique({
where: { id: activity.createdById },
select: { id: true, mobile: true },
});
if (creator) recipients.set(creator.id, creator.mobile);
}
const personName = activity.person
? `${activity.person.firstName} ${activity.person.lastName}`.trim()
: '';
const related = personName || activity.company?.name || '';
const body = related ? `${activity.title} — ${related}` : activity.title;
const stale =
!!activity.reminderAt &&
now.getTime() - activity.reminderAt.getTime() > REMINDER_SMS_MAX_DELAY_MS;
for (const [userId, mobile] of recipients) {
if (activity.reminderNotifyApp || !activity.reminderNotifySms) {
await this.create({
organizationId: activity.organizationId,
userId,
type: 'activity_reminder',
title: 'یادآوری فعالیت',
body,
link: `/activities/${activity.id}`,
entityType: 'activity',
entityId: activity.id,
});
}
if (activity.reminderNotifySms && !stale) {
await this.sendActivityReminderSms(mobile, {
title: activity.title,
related,
relatedMobile: activity.person?.mobile ?? null,
dueAt: activity.dueAt,
});
}
}
}
}
private async processNoteReminders(): Promise<void> {
const now = new Date();
const dueNotes = await this.prisma.note.findMany({
where: {
organizationId,
authorId: userId,
reminderAt: { lte: now },
where: { reminderAt: { lte: now } },
select: {
id: true,
organizationId: true,
authorId: true,
title: true,
body: true,
reminderAt: true,
},
select: { id: true, title: true, body: true },
take: 20,
take: 200,
});
for (const note of dueNotes) {
const claimed = await this.prisma.note.updateMany({
where: { id: note.id, reminderAt: note.reminderAt },
data: { reminderAt: null },
});
if (!claimed.count || !note.authorId) continue;
await this.create({
organizationId,
userId,
organizationId: note.organizationId,
userId: note.authorId,
type: 'note_reminder',
title: 'یادآوری یادداشت',
body: note.title || note.body.slice(0, 80),
link: '/notes',
entityType: 'note',
entityId: note.id,
dedupe: true,
});
await this.prisma.note.update({
where: { id: note.id },
data: { reminderAt: null },
});
}
}
private async sendActivityReminderSms(
mobile: string | null | undefined,
title: string,
info: { title: string; related: string; relatedMobile: string | null; dueAt: Date | null },
) {
if (!mobile) {
this.logger.warn('Activity reminder SMS skipped: assignee has no mobile');
@@ -229,7 +306,14 @@ export class NotificationsService {
this.logger.warn('Activity reminder SMS skipped: SMS not configured');
return;
}
const text = `${brand.reminderPrefixFa} ${title}`.slice(0, 160);
const lines = [`${brand.reminderPrefixFa} ${info.title}`];
if (info.related) {
lines.push(
info.relatedMobile ? `مشتری: ${info.related} (${info.relatedMobile})` : `مشتری: ${info.related}`,
);
}
if (info.dueAt) lines.push(`زمان: ${formatTehranDateTime(info.dueAt)}`);
const text = lines.join('\n').slice(0, 300);
const result = await this.sms.sendSmsDetailed(mobile, text);
if (!result.ok) {
this.logger.warn(`Activity reminder SMS failed: ${result.reason ?? 'unknown'}`);