feat: log outbound SMS and show history in compose modal
Persist manual, campaign, and automation SMS; expose history APIs for people and leads. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import type {
|
||||
OutboundSmsHistoryResponse,
|
||||
OutboundSmsLogDto,
|
||||
OutboundSmsSource,
|
||||
OutboundSmsStatus,
|
||||
} from '@mento/shared';
|
||||
import { OUTBOUND_SMS_SOURCE_LABELS } from '@mento/shared';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
export type RecordOutboundSmsInput = {
|
||||
organizationId: string;
|
||||
personId?: string | null;
|
||||
leadId?: string | null;
|
||||
mobile: string;
|
||||
body: string;
|
||||
source: OutboundSmsSource;
|
||||
sourceId?: string | null;
|
||||
status: OutboundSmsStatus;
|
||||
error?: string | null;
|
||||
actorUserId?: string | null;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class OutboundSmsLogService {
|
||||
private readonly logger = new Logger(OutboundSmsLogService.name);
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/** ثبت لاگ؛ خطا را میبلعد تا ارسال اصلی قطع نشود. */
|
||||
async record(input: RecordOutboundSmsInput): Promise<void> {
|
||||
try {
|
||||
if (!input.mobile?.trim() || !input.body?.trim()) return;
|
||||
if (!input.personId && !input.leadId) return;
|
||||
await this.prisma.outboundSmsLog.create({
|
||||
data: {
|
||||
organizationId: input.organizationId,
|
||||
personId: input.personId ?? null,
|
||||
leadId: input.leadId ?? null,
|
||||
mobile: input.mobile.trim(),
|
||||
body: input.body.trim().slice(0, 2000),
|
||||
source: input.source,
|
||||
sourceId: input.sourceId ?? null,
|
||||
status: input.status,
|
||||
error: input.error?.slice(0, 500) ?? null,
|
||||
actorUserId: input.actorUserId ?? null,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.warn(
|
||||
`OutboundSmsLog record failed: ${e instanceof Error ? e.message : String(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async historyForPerson(
|
||||
organizationId: string,
|
||||
personId: string,
|
||||
limit = 50,
|
||||
): Promise<OutboundSmsHistoryResponse> {
|
||||
const take = Math.min(100, Math.max(1, limit));
|
||||
const rows = await this.prisma.outboundSmsLog.findMany({
|
||||
where: { organizationId, personId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take,
|
||||
});
|
||||
return { items: rows.map((r) => this.toDto(r)) };
|
||||
}
|
||||
|
||||
async historyForLead(
|
||||
organizationId: string,
|
||||
leadId: string,
|
||||
limit = 50,
|
||||
): Promise<OutboundSmsHistoryResponse> {
|
||||
const take = Math.min(100, Math.max(1, limit));
|
||||
const rows = await this.prisma.outboundSmsLog.findMany({
|
||||
where: { organizationId, leadId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take,
|
||||
});
|
||||
return { items: rows.map((r) => this.toDto(r)) };
|
||||
}
|
||||
|
||||
private toDto(row: {
|
||||
id: string;
|
||||
body: string;
|
||||
mobile: string;
|
||||
source: string;
|
||||
status: string;
|
||||
createdAt: Date;
|
||||
}): OutboundSmsLogDto {
|
||||
const source = (
|
||||
row.source === 'campaign' || row.source === 'automation'
|
||||
? row.source
|
||||
: 'manual'
|
||||
) as OutboundSmsSource;
|
||||
const status = (row.status === 'failed' ? 'failed' : 'sent') as OutboundSmsStatus;
|
||||
return {
|
||||
id: row.id,
|
||||
body: row.body,
|
||||
mobile: row.mobile,
|
||||
source,
|
||||
sourceLabel: OUTBOUND_SMS_SOURCE_LABELS[source],
|
||||
status,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { OutboundSmsLogService } from './outbound-sms-log.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [OutboundSmsLogService],
|
||||
exports: [OutboundSmsLogService],
|
||||
})
|
||||
export class OutboundSmsModule {}
|
||||
Reference in New Issue
Block a user