chore: add ai service

This commit is contained in:
mahyargdz
2025-07-23 17:00:47 +03:30
parent 95ed0605bb
commit f42280846c
19 changed files with 1030 additions and 652 deletions
+1
View File
@@ -64,6 +64,7 @@
"dotenv": "^16.6.1",
"fastify": "^5.4.0",
"nodemailer": "^7.0.4",
"openai": "^5.10.2",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
+17
View File
@@ -110,6 +110,9 @@ importers:
nodemailer:
specifier: ^7.0.4
version: 7.0.4
openai:
specifier: ^5.10.2
version: 5.10.2
passport-jwt:
specifier: ^4.0.1
version: 4.0.1
@@ -4594,6 +4597,18 @@ packages:
resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
engines: {node: '>=8'}
openai@5.10.2:
resolution: {integrity: sha512-n+vi74LzHtvlKcDPn9aApgELGiu5CwhaLG40zxLTlFQdoSJCLACORIPC2uVQ3JEYAbqapM+XyRKFy2Thej7bIw==}
hasBin: true
peerDependencies:
ws: ^8.18.0
zod: ^3.23.8
peerDependenciesMeta:
ws:
optional: true
zod:
optional: true
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -11232,6 +11247,8 @@ snapshots:
is-wsl: 2.2.0
optional: true
openai@5.10.2: {}
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
+2
View File
@@ -14,6 +14,7 @@ import { jwtConfig } from "./configs/jwt.config";
import { databaseConfig } from "./configs/mikro-orm.config";
import { rateLimitConfig } from "./configs/rateLimit.config";
import { HTTPLogger } from "./core/middlewares/logger.middleware";
import { AiAssistantModule } from "./modules/ai-assistant/ai-assistant.module";
import { AuthModule } from "./modules/auth/auth.module";
import { DomainsModule } from "./modules/domains/domains.module";
import { EmailModule } from "./modules/email/email.module";
@@ -37,6 +38,7 @@ import { UtilsModule } from "./modules/utils/utils.module";
ThrottlerModule.forRootAsync(rateLimitConfig()),
MikroOrmModule.forRootAsync(databaseConfig),
UtilsModule,
AiAssistantModule,
AuthModule,
UsersModule,
DomainsModule,
+1
View File
@@ -1,4 +1,5 @@
// export const AUTH_THROTTLE = "AUTH_THROTTLE";
export const AI_CONFIG = "AI_CONFIG";
export const AUTH_THROTTLE_TTL = 1 * 60 * 1000;
export const AUTH_THROTTLE_LIMIT = 5;
export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000;
+6
View File
@@ -558,3 +558,9 @@ export const enum SettingMessageEnum {
EMAIL_MUST_BE_BOOLEAN = "تنظیمات ارسال ایمیل باید یک boolean باشد",
NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = "اعلان یافت نشد یا دسترسی آن را ندارید",
}
export const enum AiMessage {
NO_RESPONSE_FROM_AI_MODEL = "خطا در دریافت پاسخ از مدل AI",
MESSAGE_NOT_FOUND_OR_ACCESS_DENIED = "ایمیل یافت نشد یا دسترسی آن را ندارید",
EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED = "محتوای ایمیل خالی است یا قابل استخراج نیست",
}
+152
View File
@@ -0,0 +1,152 @@
import { FactoryProvider } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { AI_CONFIG } from "../common/constants";
export interface AIConfig {
baseUrl: string;
apiKey: string;
defaultModel: string;
temperature: number;
maxTokens: number;
timeout: number;
farsiPrompts: FarsiPrompts;
}
export interface FarsiPrompts {
summarization: {
systemPrompt: string;
promptTemplate: {
intro: string;
subjectLabel: string;
fromLabel: string;
toLabel: string;
dateLabel: string;
contentLabel: string;
attachmentsLabel: string;
analysisRequirements: string;
summaryLengthLabel: string;
toneLabel: string;
};
};
writingAssistance: {
systemPrompt: string;
promptTemplate: {
intro: string;
mainMessageLabel: string;
emailDetailsLabel: string;
purposeLabel: string;
toneLabel: string;
lengthLabel: string;
keyPointsLabel: string;
contextLabel: string;
originalSubjectLabel: string;
originalSenderLabel: string;
originalContentLabel: string;
additionalInstructionsLabel: string;
};
};
}
const FARSI_PROMPTS: FarsiPrompts = {
summarization: {
systemPrompt: `شما یک تحلیلگر و خلاصه‌کننده ماهر ایمیل هستید. وظیفه شما تحلیل و خلاصه‌سازی ایمیل‌ها به صورت حرفه‌ای و دقیق است.
الزامات:
- محتوای ایمیل را تحلیل کرده و خلاصه‌ای جامع ارائه دهید
- نکات کلیدی که در ایمیل ذکر شده‌اند را استخراج کنید
- بر اساس محتوای ایمیل، اقدامات قابل انجام پیشنهاد دهید
- سطح اولویت (کم، متوسط، زیاد) را بر اساس شاخص‌های اضطراری تعیین کنید
- احساسات (مثبت، خنثی، منفی) ایمیل را تحلیل کنید
- از لحن {tone} در تحلیل خود استفاده کنید
- سطح جزئیات {length} ارائه دهید
- به زبان {language} پاسخ دهید
فرمت پاسخ: شیء JSON با ساختار زیر:
{
"summary": "خلاصه مختصر ایمیل",
"keyPoints": ["نکته کلیدی ۱", "نکته کلیدی ۲", ...],
"suggestedActions": ["اقدام ۱", "اقدام ۲", ...],
"priority": "low|medium|high",
"sentiment": "positive|neutral|negative"
}`,
promptTemplate: {
intro: "لطفاً این ایمیل را تحلیل و خلاصه کنید:",
subjectLabel: "موضوع:",
fromLabel: "از:",
toLabel: "به:",
dateLabel: "تاریخ:",
contentLabel: "محتوا:",
attachmentsLabel: "پیوست‌ها:",
analysisRequirements: "الزامات تحلیل:",
summaryLengthLabel: "- طول خلاصه:",
toneLabel: "- لحن:",
},
},
writingAssistance: {
systemPrompt: `شما یک دستیار ماهر نگارش ایمیل هستید. وظیفه شما کمک به کاربران برای نوشتن ایمیل‌های حرفه‌ای و مؤثر است.
الزامات:
- بر اساس قصد و زمینه کاربر، محتوای ایمیل تولید کنید
- در تمام ایمیل از لحن {tone} استفاده کنید
- ایمیل با طول {length} بسازید
- هدف: {purpose}
- نکات و بهبودهای مفید ارائه دهید
- در صورت امکان، نسخه‌های جایگزین تولید کنید
- به زبان {language} پاسخ دهید
فرمت پاسخ: شیء JSON با ساختار زیر:
{
"subject": "موضوع پیشنهادی ایمیل",
"content": "محتوای کامل ایمیل",
"alternatives": ["نسخه جایگزین ۱", "نسخه جایگزین ۲", ...],
"tips": ["نکته نگارش ۱", "نکته نگارش ۲", ...],
"improvements": ["پیشنهاد بهبود ۱", "پیشنهاد بهبود ۲", ...]
}`,
promptTemplate: {
intro: "لطفاً در نوشتن ایمیل با الزامات زیر به من کمک کنید:",
mainMessageLabel: "پیام/قصد اصلی:",
emailDetailsLabel: "جزئیات ایمیل:",
purposeLabel: "- هدف:",
toneLabel: "- لحن:",
lengthLabel: "- طول:",
keyPointsLabel: "نکات کلیدی برای درج:",
contextLabel: "زمینه از ایمیل اصلی:",
originalSubjectLabel: "- موضوع اصلی:",
originalSenderLabel: "- فرستنده اصلی:",
originalContentLabel: "- محتوای اصلی:",
additionalInstructionsLabel: "دستورالعمل‌های اضافی:",
},
},
};
export const aiConfigProvider: FactoryProvider<AIConfig> = {
provide: AI_CONFIG,
useFactory: (configService: ConfigService): AIConfig => ({
baseUrl: configService.get<string>("AI_BASE_URL", "https://api.openai.com/v1"),
apiKey: configService.get<string>("AI_API_KEY", ""),
defaultModel: configService.get<string>("AI_DEFAULT_MODEL", "gpt-3.5-turbo"),
temperature: configService.get<number>("AI_TEMPERATURE", 0.7),
maxTokens: configService.get<number>("AI_MAX_TOKENS", 2000),
timeout: configService.get<number>("AI_TIMEOUT", 30000),
farsiPrompts: FARSI_PROMPTS,
}),
inject: [ConfigService],
};
// Available models for different providers
export const AI_MODELS = {
OPENAI: {
GPT_3_5_TURBO: "gpt-3.5-turbo",
GPT_4: "gpt-4",
GPT_4_TURBO: "gpt-4-turbo-preview",
},
GEMINI: {
GEMINI_2_0_FLASH: "google/gemini-2.0-flash-001",
GEMINI_2_5_PRO: "google/gemini-2.5-pro-preview",
GEMINI_2_5_FLASH: "google/gemini-2.5-flash-preview",
GEMINI_2_0_FLASH_LITE: "google/gemini-2.0-flash-lite-001",
GEMINI_FLASH_1_5: "google/gemini-flash-1.5",
GEMINI_FLASH_1_5_8B: "google/gemini-flash-1.5-8b",
},
} as const;
-49
View File
@@ -1,49 +0,0 @@
import { ConfigService } from "@nestjs/config";
export function smsConfigs() {
return {
inject: [ConfigService],
useFactory(configService: ConfigService) {
return {
API_URL: configService.getOrThrow<string>("SMS_API_URL"),
API_KEY: configService.getOrThrow<string>("SMS_API_KEY"),
SMS_PATTERN_OTP: configService.getOrThrow<string>("SMS_PATTERN_OTP"),
SMS_PATTERN_INVOICE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE"),
SMS_PATTERN_LOGIN: configService.getOrThrow<string>("SMS_PATTERN_LOGIN"),
SMS_PATTERN_INVOICE_CREATED: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_CREATED"),
SMS_PATTERN_ANNOUNCEMENT: configService.getOrThrow<string>("SMS_PATTERN_ANNOUNCEMENT"),
SMS_PATTERN_TICKET_CREATED: configService.getOrThrow<string>("SMS_PATTERN_TICKET_CREATED"),
SMS_PATTERN_TICKET_ASSIGNED_ADMIN: configService.getOrThrow<string>("SMS_PATTERN_TICKET_ASSIGNED_ADMIN"),
SMS_PATTERN_INVOICE_APPROVED: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_APPROVED"),
SMS_PATTERN_INVOICE_PAID: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_PAID"),
SMS_PATTERN_RECURRING_INVOICE_DRAFT: configService.getOrThrow<string>("SMS_PATTERN_RECURRING_INVOICE_DRAFT"),
SMS_PATTERN_SUBSCRIPTION_CANCELLED: configService.getOrThrow<string>("SMS_PATTERN_SUBSCRIPTION_CANCELLED"),
SMS_PATTERN_INVOICE_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_REMINDER"),
SMS_PATTERN_INVOICE_OVERDUE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_OVERDUE"),
SMS_PATTERN_PAYMENT_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_PAYMENT_REMINDER"),
SMS_PATTERN_PAYMENT_CANCELLATION: configService.getOrThrow<string>("SMS_PATTERN_PAYMENT_CANCELLATION"),
};
},
};
}
export interface ISmsConfigs {
API_URL: string;
API_KEY: string;
SMS_PATTERN_OTP: string;
SMS_PATTERN_INVOICE: string;
SMS_PATTERN_LOGIN: string;
SMS_PATTERN_INVOICE_CREATED: string;
SMS_PATTERN_ANNOUNCEMENT: string;
SMS_PATTERN_TICKET_CREATED: string;
SMS_PATTERN_TICKET_ANSWERED: string;
SMS_PATTERN_TICKET_ASSIGNED_ADMIN: string;
SMS_PATTERN_INVOICE_APPROVED: string;
SMS_PATTERN_INVOICE_PAID: string;
SMS_PATTERN_RECURRING_INVOICE_DRAFT: string;
SMS_PATTERN_SUBSCRIPTION_CANCELLED: string;
SMS_PATTERN_INVOICE_REMINDER: string;
SMS_PATTERN_INVOICE_OVERDUE: string;
SMS_PATTERN_PAYMENT_REMINDER: string;
SMS_PATTERN_PAYMENT_CANCELLATION: string;
}
@@ -0,0 +1,26 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString } from "class-validator";
import { SummaryLength, SummaryTone } from "../enums/ai.enum";
export class SummarizeEmailDto {
@IsNotEmpty({ message: "Message ID is required" })
@IsNumber({}, { message: "Message ID must be a number" })
@ApiProperty({ description: "The message ID to fetch and summarize from the mail server", example: 12345 })
messageId: number;
@IsNotEmpty({ message: "Mailbox ID is required" })
@IsString({ message: "Mailbox ID must be a string" })
@ApiProperty({ description: "The mailbox ID where the message is located", example: "507f1f77bcf86cd799439012" })
mailbox: string;
@IsOptional()
@IsEnum(SummaryLength, { message: "Summary length must be one of: short, medium, detailed" })
@ApiPropertyOptional({ description: "Preferred summary length", enum: SummaryLength, default: SummaryLength.MEDIUM })
length?: SummaryLength;
@IsOptional()
@IsEnum(SummaryTone, { message: "Summary tone must be one of: formal, casual, professional" })
@ApiPropertyOptional({ description: "Preferred summary tone", enum: SummaryTone, default: SummaryTone.PROFESSIONAL })
tone?: SummaryTone;
}
@@ -0,0 +1,146 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsArray, IsEnum, IsNotEmpty, IsOptional, IsString, ValidateNested } from "class-validator";
import { EmailLength, EmailPurpose, WritingTone } from "../enums/ai.enum";
export class EmailContextDto {
@IsOptional()
@IsString({ message: "Original email content must be a string" })
@ApiPropertyOptional({
description: "Original email content (for replies/forwards)",
example: "Hi, I need to discuss the project timeline...",
})
originalContent?: string;
@IsOptional()
@IsString({ message: "Original sender must be a string" })
@ApiPropertyOptional({
description: "Original sender (for replies/forwards)",
example: "john.doe@company.com",
})
originalSender?: string;
@IsOptional()
@IsString({ message: "Original subject must be a string" })
@ApiPropertyOptional({
description: "Original subject (for replies/forwards)",
example: "Project Timeline Discussion",
})
originalSubject?: string;
}
export class WriteEmailAssistanceDto {
@IsNotEmpty({ message: "Email purpose is required" })
@IsEnum(EmailPurpose, { message: "Email purpose must be one of: reply, forward, new, follow_up" })
@ApiProperty({
description: "Purpose of the email",
enum: EmailPurpose,
example: EmailPurpose.REPLY,
})
purpose: EmailPurpose;
@IsNotEmpty({ message: "Main message or intent is required" })
@IsString({ message: "Main message must be a string" })
@ApiProperty({
description: "Main message or intent of the email",
example: "I want to schedule a meeting to discuss the project timeline and address any concerns.",
})
mainMessage: string;
@IsOptional()
@IsString({ message: "Recipient information must be a string" })
@ApiPropertyOptional({
description: "Recipient information (optional)",
example: "john.doe@company.com",
})
recipient?: string;
@IsOptional()
@IsEnum(WritingTone, { message: "Writing tone must be one of: formal, casual, professional, friendly, urgent" })
@ApiPropertyOptional({
description: "Desired tone of the email",
enum: WritingTone,
default: WritingTone.PROFESSIONAL,
})
tone?: WritingTone;
@IsOptional()
@IsEnum(EmailLength, { message: "Email length must be one of: short, medium, long" })
@ApiPropertyOptional({
description: "Desired length of the email",
enum: EmailLength,
default: EmailLength.MEDIUM,
})
length?: EmailLength;
@IsOptional()
@IsArray({ message: "Key points must be an array" })
@IsString({ each: true, message: "Each key point must be a string" })
@ApiPropertyOptional({
description: "Key points to include in the email",
type: [String],
example: ["Discuss project timeline", "Address resource allocation", "Set next meeting date"],
})
keyPoints?: string[];
@IsOptional()
@ValidateNested()
@Type(() => EmailContextDto)
@ApiPropertyOptional({
description: "Context from original email (for replies/forwards)",
type: EmailContextDto,
})
context?: EmailContextDto;
@IsOptional()
@IsString({ message: "Additional instructions must be a string" })
@ApiPropertyOptional({
description: "Additional instructions or preferences",
example: "Please keep it concise and mention the deadline",
})
additionalInstructions?: string;
}
export class EmailSuggestionDto {
@ApiProperty({
description: "Suggested email subject",
example: "Re: Project Timeline Discussion - Meeting Request",
})
subject: string;
@ApiProperty({
description: "Generated email content",
example: "Dear John,\n\nThank you for your email regarding the project timeline...",
})
content: string;
@ApiProperty({
description: "Alternative variations of the email",
type: [String],
example: ["More formal version", "Shorter version", "More detailed version"],
})
alternatives: string[];
}
export class EmailWritingResponseDto {
@ApiProperty({
description: "Primary email suggestion",
type: EmailSuggestionDto,
})
suggestion: EmailSuggestionDto;
@ApiProperty({
description: "Writing tips and recommendations",
type: [String],
example: ["Consider adding a specific deadline", "You might want to include meeting agenda", "Don't forget to CC relevant stakeholders"],
})
tips: string[];
@ApiProperty({
description: "Suggested improvements for the original request",
type: [String],
example: ["Consider being more specific about the timeline", "Add context about project urgency"],
})
improvements: string[];
}
+217
View File
@@ -0,0 +1,217 @@
# AI Assistant Module
The AI assistant module provides email summarization and writing assistance functionality using OpenAI/Google Gemini models through the [Liara AI platform](https://docs.liara.ir/ai/google-gemini/).
## Features
- **Email Summarization**: Analyze emails and extract key points, suggested actions, priority levels, and sentiment analysis
- **Email Writing Assistance**: Generate professional emails with AI assistance based on context and purpose
- **Multi-Model Support**: Compatible with OpenAI GPT models and Google Gemini models
- **Flexible Configuration**: Configurable tone, length, and purpose for different use cases
## Setup
### 1. Install Dependencies
```bash
pnpm add openai
```
### 2. Environment Variables
Add the following environment variables to your `.env` file:
```env
# AI Service Configuration
AI_BASE_URL=https://api.openai.com/v1
AI_API_KEY=your_api_key_here
AI_DEFAULT_MODEL=gpt-3.5-turbo
AI_TEMPERATURE=0.7
AI_MAX_TOKENS=2000
AI_TIMEOUT=30000
```
### For Liara AI (Google Gemini):
```env
# Liara AI Configuration
AI_BASE_URL=https://api.liara.ir/v1
AI_API_KEY=your_liara_api_key
AI_DEFAULT_MODEL=google/gemini-2.0-flash-001
AI_TEMPERATURE=0.7
AI_MAX_TOKENS=2000
AI_TIMEOUT=30000
```
### Available Models
#### OpenAI Models:
- `gpt-3.5-turbo`
- `gpt-4`
- `gpt-4-turbo-preview`
#### Google Gemini Models (via Liara):
- `google/gemini-2.0-flash-001`
- `google/gemini-2.5-pro-preview`
- `google/gemini-2.5-flash-preview`
- `google/gemini-2.0-flash-lite-001`
- `google/gemini-flash-1.5`
- `google/gemini-flash-1.5-8b`
## API Endpoints
### Email Summarization
```http
POST /ai-assistant/summarize-email
Content-Type: application/json
x-business-id: your-business-id
Authorization: Bearer your-token
{
"messageId": 12345,
"mailbox": "507f1f77bcf86cd799439012",
"length": "medium",
"tone": "professional"
}
```
**Response:**
```json
{
"summary": "John is requesting a meeting to discuss the upcoming project timeline and deliverables.",
"keyPoints": ["Project timeline discussion", "Meeting request", "Deliverables review"],
"suggestedActions": ["Schedule meeting with John", "Prepare project timeline document", "Review current deliverables"],
"priority": "medium",
"sentiment": "neutral"
}
```
### Email Writing Assistance
```http
POST /ai-assistant/write-email
Content-Type: application/json
x-business-id: your-business-id
Authorization: Bearer your-token
{
"purpose": "reply",
"mainMessage": "I want to schedule a meeting to discuss the project timeline",
"tone": "professional",
"length": "medium",
"keyPoints": [
"Discuss timeline",
"Address concerns"
],
"context": {
"originalSubject": "Project Update",
"originalSender": "john@example.com",
"originalContent": "Hi, I need to discuss the project timeline..."
},
"additionalInstructions": "Please keep it concise and mention the deadline"
}
```
**Response:**
```json
{
"suggestion": {
"subject": "Re: Project Update - Meeting Request",
"content": "Hello,\n\nThank you for your email regarding the project timeline...",
"alternatives": ["More concise version available", "More detailed version available", "Alternative tone available"]
},
"tips": ["Consider adding a specific deadline", "You might want to include meeting agenda"],
"improvements": ["Consider being more specific about the timeline", "Add context about project urgency"]
}
```
## Configuration Options
### Summarization Options
- **Length**: `short`, `medium`, `detailed`
- **Tone**: `formal`, `casual`, `professional`
### Writing Assistance Options
- **Purpose**: `reply`, `forward`, `new`, `follow_up`
- **Tone**: `formal`, `casual`, `professional`, `friendly`, `urgent`
- **Length**: `short`, `medium`, `long`
## Architecture
The AI assistant module consists of:
1. **AiService**: Core service that communicates with AI models
2. **AiAssistantService**: Business logic layer that handles email processing
3. **AiAssistantController**: API endpoints for client interactions
4. **DTOs**: Data transfer objects for request/response validation
5. **Interfaces**: Type definitions for AI service interactions
## Integration with Liara AI
This module is designed to work seamlessly with [Liara's AI platform](https://docs.liara.ir/ai/google-gemini/), which provides:
- OpenAI-compatible API
- Multiple AI model support
- Persian language support
- Scalable infrastructure
## Error Handling
The module includes comprehensive error handling:
- **Message Not Found**: Returns 404 when email message doesn't exist
- **Empty Content**: Returns 400 when email content is empty
- **AI Service Errors**: Logs and re-throws AI service errors
- **Authentication**: Validates user permissions and business context
## Security
- All endpoints require authentication
- Business context validation
- Rate limiting (inherited from global configuration)
- Input validation and sanitization
## Usage Example
```typescript
// In your service
import { AiAssistantService } from "./modules/ai-assistant/services/ai-assistant.service";
@Injectable()
export class EmailProcessorService {
constructor(private readonly aiAssistant: AiAssistantService) {}
async processIncomingEmail(messageId: number, mailboxId: string, userId: string) {
const summary = await this.aiAssistant.summarizeEmail(
{
messageId,
mailbox: mailboxId,
length: "medium",
tone: "professional",
},
userId,
);
// Process the summary...
return summary;
}
}
```
## Contributing
When adding new AI features:
1. Update the appropriate interface in `interfaces/ai-service.interface.ts`
2. Implement the logic in `AiService`
3. Add business logic in `AiAssistantService`
4. Create API endpoints in `AiAssistantController`
5. Add proper validation DTOs
6. Update this documentation
@@ -0,0 +1,35 @@
import { Body, Controller, Post } from "@nestjs/common";
import { ApiOperation, ApiResponse } from "@nestjs/swagger";
import { SummarizeEmailDto } from "./DTO/summarize-email.dto";
import { WriteEmailAssistanceDto } from "./DTO/write-email-assistance.dto";
import { AiAssistantService } from "./services/ai-assistant.service";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { UserDec } from "../../common/decorators/user.decorator";
@Controller("ai-assistant")
// @ApiHeader({ name: "x-business-id", description: "Business ID" })
// @UseInterceptors(BusinessInterceptor)
@AuthGuards()
export class AiAssistantController {
constructor(private readonly aiAssistantService: AiAssistantService) {}
@Post("summarize-email")
@ApiOperation({ summary: "Summarize an email using AI" })
@ApiResponse({ status: 201, description: "Email successfully summarized" })
@ApiResponse({ status: 400, description: "Bad request - invalid message ID or mailbox" })
@ApiResponse({ status: 404, description: "Message not found or access denied" })
@ApiResponse({ status: 401, description: "Unauthorized - invalid authentication" })
async summarizeEmail(@Body() summarizeDto: SummarizeEmailDto, @UserDec("wildduckUserId") wildduckUserId: string) {
return this.aiAssistantService.summarizeEmail(summarizeDto, wildduckUserId);
}
@Post("write-email")
@ApiOperation({ summary: "Get AI assistance for writing emails" })
@ApiResponse({ status: 201, description: "Email writing assistance generated successfully" })
@ApiResponse({ status: 400, description: "Bad request - invalid input data" })
@ApiResponse({ status: 401, description: "Unauthorized - invalid authentication" })
async assistEmailWriting(@Body() writeDto: WriteEmailAssistanceDto, @UserDec("wildduckUserId") wildduckUserId: string) {
return this.aiAssistantService.assistEmailWriting(writeDto, wildduckUserId);
}
}
@@ -0,0 +1,15 @@
import { Module } from "@nestjs/common";
import { AiAssistantController } from "./ai-assistant.controller";
import { AiAssistantService } from "./services/ai-assistant.service";
import { AiService } from "./services/ai.service";
import { aiConfigProvider } from "../../configs/ai.config";
import { MailServerModule } from "../mail-server/mail-server.module";
@Module({
imports: [MailServerModule],
controllers: [AiAssistantController],
providers: [AiAssistantService, AiService, aiConfigProvider],
exports: [AiAssistantService, AiService],
})
export class AiAssistantModule {}
+32
View File
@@ -0,0 +1,32 @@
export enum SummaryLength {
SHORT = "short",
MEDIUM = "medium",
DETAILED = "detailed",
}
export enum SummaryTone {
FORMAL = "formal",
CASUAL = "casual",
PROFESSIONAL = "professional",
}
export enum EmailPurpose {
REPLY = "reply",
FORWARD = "forward",
NEW = "new",
FOLLOW_UP = "follow_up",
}
export enum WritingTone {
FORMAL = "formal",
CASUAL = "casual",
PROFESSIONAL = "professional",
FRIENDLY = "friendly",
URGENT = "urgent",
}
export enum EmailLength {
SHORT = "short",
MEDIUM = "medium",
LONG = "long",
}
@@ -0,0 +1,50 @@
export interface EmailData {
subject: string;
content: string;
htmlContent?: string;
from: string;
to: string[];
date: Date;
attachments?: unknown[];
}
export interface SummarizationOptions {
length: "short" | "medium" | "detailed";
tone: "formal" | "casual" | "professional";
language?: "Persian" | "English";
}
export interface WritingAssistanceOptions {
purpose: "reply" | "forward" | "new" | "follow_up";
tone: "formal" | "casual" | "professional" | "friendly" | "urgent";
length: "short" | "medium" | "long";
keyPoints?: string[];
context?: {
originalContent?: string;
originalSender?: string;
originalSubject?: string;
};
additionalInstructions?: string;
language?: "Persian" | "English";
}
export interface AISummaryResponse {
summary: string;
keyPoints: string[];
suggestedActions: string[];
priority: "low" | "medium" | "high";
sentiment: "positive" | "neutral" | "negative";
}
export interface AIWritingResponse {
subject: string;
content: string;
alternatives: string[];
tips: string[];
improvements: string[];
}
export interface AIServiceInterface {
summarizeEmail(emailData: EmailData, options: SummarizationOptions): Promise<AISummaryResponse>;
assistEmailWriting(mainMessage: string, options: WritingAssistanceOptions): Promise<AIWritingResponse>;
}
@@ -0,0 +1,162 @@
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { firstValueFrom } from "rxjs";
import { AiService } from "./ai.service";
import { AiMessage } from "../../../common/enums/message.enum";
import { MessageDetails } from "../../mail-server/interfaces/messages-response.interface";
import { WildDuckMessagesService } from "../../mail-server/services/wildduck-messages.service";
import { SummarizeEmailDto } from "../DTO/summarize-email.dto";
import { WriteEmailAssistanceDto } from "../DTO/write-email-assistance.dto";
import { SummaryLength, SummaryTone } from "../enums/ai.enum";
import { EmailData, SummarizationOptions, WritingAssistanceOptions } from "../interfaces/ai-service.interface";
@Injectable()
export class AiAssistantService {
constructor(
private readonly messagesService: WildDuckMessagesService,
private readonly aiService: AiService,
) {}
//************************************************* */
async summarizeEmail(summarizeDto: SummarizeEmailDto, userId: string) {
const message = await firstValueFrom(this.messagesService.getMessage(userId, summarizeDto.mailbox, summarizeDto.messageId));
if (!message) throw new NotFoundException(AiMessage.MESSAGE_NOT_FOUND_OR_ACCESS_DENIED);
const emailData = this.extractEmailData(message);
const summary = await this.generateEmailSummary(emailData, summarizeDto.length, summarizeDto.tone);
return summary;
}
//************************************************* */
async assistEmailWriting(writeAssistanceDto: WriteEmailAssistanceDto, userId: string) {
const assistance = await this.generateEmailWritingAssistance(writeAssistanceDto, userId);
return assistance;
}
//************************************************* */
private extractEmailData(message: MessageDetails) {
return {
subject: message.subject,
content: this.extractTextContent(message),
htmlContent: this.extractHtmlContent(message),
from: this.extractSenderInfo(message),
to: this.extractRecipientInfo(message),
date: message.date || new Date(),
attachments: message.attachments || [],
};
}
//************************************************* */
private extractTextContent(message: MessageDetails): string {
if (message.text) {
return typeof message.text === "string" ? message.text : message.text || "";
}
if (message.html) {
return this.stripHtmlTags(typeof message.html === "string" ? message.html : message.html[0] || "");
}
return "";
}
//************************************************* */
private extractHtmlContent(message: MessageDetails): string {
if (message.html) {
return typeof message.html === "string" ? message.html : message.html[0] || "";
}
return "";
}
//************************************************* */
private extractSenderInfo(message: MessageDetails): string {
if (message.from && message.from.address) {
return message.from.name ? `${message.from.name} <${message.from.address}>` : message.from.address;
}
return "";
}
//************************************************* */
private extractRecipientInfo(message: MessageDetails): string[] {
const recipients: string[] = [];
if (message.to && Array.isArray(message.to)) {
recipients.push(...message.to.map((recipient) => (recipient.name ? `${recipient.name} <${recipient.address}>` : recipient.address)));
}
return recipients;
}
//************************************************* */
private stripHtmlTags(html: string): string {
return html
.replace(/<[^>]*>/g, "")
.replace(/&nbsp;/g, " ")
.trim();
}
//************************************************* */
private async generateEmailSummary(emailData: any, length: SummaryLength = SummaryLength.MEDIUM, _tone: SummaryTone = SummaryTone.PROFESSIONAL) {
const content = emailData.content || emailData.htmlContent;
if (!content.trim()) throw new BadRequestException(AiMessage.EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED);
const aiEmailData: EmailData = {
subject: emailData.subject,
content: emailData.content,
htmlContent: emailData.htmlContent,
from: emailData.from,
to: emailData.to,
date: emailData.date,
attachments: emailData.attachments,
};
const summarizationOptions: SummarizationOptions = {
length: length.toLowerCase() as "short" | "medium" | "detailed",
tone: _tone.toLowerCase() as "formal" | "casual" | "professional",
};
const aiResponse = await this.aiService.summarizeEmail(aiEmailData, summarizationOptions);
return {
summary: aiResponse.summary,
keyPoints: aiResponse.keyPoints,
suggestedActions: aiResponse.suggestedActions,
priority: aiResponse.priority,
sentiment: aiResponse.sentiment,
};
}
//************************************************* */
private async generateEmailWritingAssistance(writeAssistanceDto: WriteEmailAssistanceDto, _userId: string) {
const writingOptions: WritingAssistanceOptions = {
purpose: writeAssistanceDto.purpose.toLowerCase() as "reply" | "forward" | "new" | "follow_up",
tone: (writeAssistanceDto.tone?.toLowerCase() as "formal" | "casual" | "professional" | "friendly" | "urgent") || "professional",
length: (writeAssistanceDto.length?.toLowerCase() as "short" | "medium" | "long") || "medium",
keyPoints: writeAssistanceDto.keyPoints,
context: writeAssistanceDto.context
? {
originalContent: writeAssistanceDto.context.originalContent,
originalSender: writeAssistanceDto.context.originalSender,
originalSubject: writeAssistanceDto.context.originalSubject,
}
: undefined,
additionalInstructions: writeAssistanceDto.additionalInstructions,
};
const aiResponse = await this.aiService.assistEmailWriting(writeAssistanceDto.mainMessage, writingOptions);
return {
suggestion: {
subject: aiResponse.subject,
content: aiResponse.content,
alternatives: aiResponse.alternatives,
},
tips: aiResponse.tips,
improvements: aiResponse.improvements,
};
}
}
@@ -0,0 +1,166 @@
import { Inject, Injectable, InternalServerErrorException } from "@nestjs/common";
import OpenAI from "openai";
import { AI_CONFIG } from "../../../common/constants";
import { AiMessage } from "../../../common/enums/message.enum";
import { AIConfig } from "../../../configs/ai.config";
import {
AIServiceInterface,
AISummaryResponse,
AIWritingResponse,
EmailData,
SummarizationOptions,
WritingAssistanceOptions,
} from "../interfaces/ai-service.interface";
@Injectable()
export class AiService implements AIServiceInterface {
private readonly openai: OpenAI;
constructor(@Inject(AI_CONFIG) private readonly aiConfig: AIConfig) {
this.openai = new OpenAI({
baseURL: this.aiConfig.baseUrl,
apiKey: this.aiConfig.apiKey,
timeout: this.aiConfig.timeout,
});
}
async summarizeEmail(emailData: EmailData, options: SummarizationOptions): Promise<AISummaryResponse> {
const prompt = this.buildSummarizationPrompt(emailData, options);
const completion = await this.openai.chat.completions.create({
model: this.aiConfig.defaultModel,
messages: [
{
role: "system",
content: this.getSummarizationSystemPrompt(options),
},
{
role: "user",
content: prompt,
},
],
temperature: this.aiConfig.temperature,
max_tokens: this.aiConfig.maxTokens,
response_format: { type: "json_object" },
});
const response = completion.choices[0]?.message?.content;
if (!response) throw new InternalServerErrorException(AiMessage.NO_RESPONSE_FROM_AI_MODEL);
const parsedResponse = JSON.parse(response) as AISummaryResponse;
return parsedResponse;
}
//************************************************* */
async assistEmailWriting(mainMessage: string, options: WritingAssistanceOptions): Promise<AIWritingResponse> {
const prompt = this.buildWritingAssistancePrompt(mainMessage, options);
const completion = await this.openai.chat.completions.create({
model: this.aiConfig.defaultModel,
messages: [
{
role: "system",
content: this.getWritingAssistanceSystemPrompt(options),
},
{
role: "user",
content: prompt,
},
],
temperature: this.aiConfig.temperature,
max_tokens: this.aiConfig.maxTokens,
response_format: { type: "json_object" },
});
const response = completion.choices[0]?.message?.content;
if (!response) throw new InternalServerErrorException(AiMessage.NO_RESPONSE_FROM_AI_MODEL);
const parsedResponse = JSON.parse(response) as AIWritingResponse;
return parsedResponse;
}
//************************************************* */
private getSummarizationSystemPrompt(options: SummarizationOptions): string {
const language = options.language || "Persian";
const template = this.aiConfig.farsiPrompts.summarization.systemPrompt;
return template.replace("{tone}", options.tone).replace("{length}", options.length).replace("{language}", language);
}
//************************************************* */
private getWritingAssistanceSystemPrompt(options: WritingAssistanceOptions): string {
const language = options.language || "Persian";
const template = this.aiConfig.farsiPrompts.writingAssistance.systemPrompt;
return template
.replace("{tone}", options.tone)
.replace("{length}", options.length)
.replace("{purpose}", options.purpose)
.replace("{language}", language);
}
//************************************************* */
private buildSummarizationPrompt(emailData: EmailData, options: SummarizationOptions): string {
const template = this.aiConfig.farsiPrompts.summarization.promptTemplate;
let prompt = `${template.intro}\n\n`;
console.log(emailData);
prompt += `${template.subjectLabel} ${emailData.subject}\n`;
prompt += `${template.fromLabel} ${emailData.from}\n`;
prompt += `${template.toLabel} ${emailData.to.join(", ")}\n`;
prompt += `${template.dateLabel} ${emailData.date}\n\n`;
prompt += `${template.contentLabel}\n${emailData.content}\n\n`;
if (emailData.attachments && emailData.attachments.length > 0) {
prompt += `${template.attachmentsLabel} ${emailData.attachments.length} فایل\n\n`;
}
prompt += `${template.analysisRequirements}\n`;
prompt += `${template.summaryLengthLabel} ${options.length}\n`;
prompt += `${template.toneLabel} ${options.tone}\n`;
return prompt;
}
//************************************************* */
private buildWritingAssistancePrompt(mainMessage: string, options: WritingAssistanceOptions): string {
const template = this.aiConfig.farsiPrompts.writingAssistance.promptTemplate;
let prompt = `${template.intro}\n\n`;
prompt += `${template.mainMessageLabel} ${mainMessage}\n\n`;
prompt += `${template.emailDetailsLabel}\n`;
prompt += `${template.purposeLabel} ${options.purpose}\n`;
prompt += `${template.toneLabel} ${options.tone}\n`;
prompt += `${template.lengthLabel} ${options.length}\n\n`;
if (options.keyPoints && options.keyPoints.length > 0) {
prompt += `${template.keyPointsLabel}\n`;
options.keyPoints.forEach((point, index) => {
prompt += `${index + 1}. ${point}\n`;
});
prompt += `\n`;
}
if (options.context) {
prompt += `${template.contextLabel}\n`;
if (options.context.originalSubject) {
prompt += `${template.originalSubjectLabel} ${options.context.originalSubject}\n`;
}
if (options.context.originalSender) {
prompt += `${template.originalSenderLabel} ${options.context.originalSender}\n`;
}
if (options.context.originalContent) {
prompt += `${template.originalContentLabel} ${options.context.originalContent}\n`;
}
prompt += `\n`;
}
if (options.additionalInstructions) {
prompt += `${template.additionalInstructionsLabel} ${options.additionalInstructions}\n\n`;
}
return prompt;
}
}
-51
View File
@@ -1,51 +0,0 @@
export interface IParameterArray {
name: TemplateParams;
value: string;
}
export interface ISmsResponse {
status: number;
message: string;
}
//----------------------------------------------
export interface ISmsGetLineResponse extends ISmsResponse {
data: string[];
}
export interface ISmsVerifyResponse extends ISmsResponse {
data: { MessageId: number; Cost: number };
}
//----------------------------------------------
export interface ISmsVerifyBody {
Parameters: IParameterArray[];
Mobile: string;
TemplateId: string;
}
export type TemplateParams =
| "VERIFICATIONCODE"
| "code"
| "invoiceId"
| "price"
| "items"
| "loginDate"
| "invoiceDate"
| "title"
| "description"
| "ticketId"
| "serviceName"
| "date"
| "amount"
| "newBalance"
| "reason"
| "dueDate"
| "lateFee"
| "paidDate"
| "user"
| "subject"
| "planName"
| "message"
| "blogTitle"
| "fullName"
| "mobile";
-542
View File
@@ -1,542 +0,0 @@
import { HttpService } from "@nestjs/axios";
import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
import { AxiosError } from "axios";
import { catchError, firstValueFrom } from "rxjs";
import { SmsMessage } from "../../../common/enums/message.enum";
import { ISmsConfigs } from "../../../configs/sms.config";
import { SMS_CONFIG } from "../constants";
import { ISmsGetLineResponse, ISmsVerifyBody, ISmsVerifyResponse } from "../interfaces/ISms";
@Injectable()
export class SmsService {
private readonly logger = new Logger(SmsService.name);
constructor(
@Inject(SMS_CONFIG) private smsConfigs: ISmsConfigs,
private readonly httpService: HttpService,
) {}
//************************************************* */
async sendSmsVerifyCode(mobile: string, otpCode: string) {
//
const smsData: ISmsVerifyBody = {
Parameters: [{ name: "code", value: otpCode }],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_OTP,
};
//
try {
const { data } = await firstValueFrom(
this.httpService
.post<ISmsVerifyResponse>(
`${this.smsConfigs.API_URL}/send/verify`,
{ ...smsData },
{
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
},
)
.pipe(
catchError((err: AxiosError) => {
this.logger.error("error in sending verify sms", err);
throw new InternalServerErrorException("error in sending verify sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
throw new InternalServerErrorException(SmsMessage.SEND_SMS_ERROR);
}
}
//************************************************* */
async sendInvoiceVerifyCode(mobile: string, otpCode: string, invoiceId: number, price: number, items: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "code", value: otpCode },
{ name: "invoiceId", value: invoiceId.toString() },
{ name: "price", value: Intl.NumberFormat("fa-IR").format(price) },
{ name: "items", value: items },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE,
};
try {
const { data } = await firstValueFrom(
this.httpService
.post<ISmsVerifyResponse>(
`${this.smsConfigs.API_URL}/send/verify`,
{ ...smsData },
{
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
},
)
.pipe(
catchError((err: AxiosError) => {
this.logger.error("error in sending verify sms", err);
throw new InternalServerErrorException("error in sending verify sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
throw new InternalServerErrorException(SmsMessage.SEND_SMS_ERROR);
}
}
//************************************************* */
async sendLoginSms(mobile: string, loginDate: string) {
const smsData: ISmsVerifyBody = {
Parameters: [{ name: "loginDate", value: loginDate }],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_LOGIN,
};
try {
const { data } = await firstValueFrom(
this.httpService
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
})
.pipe(
catchError((err: AxiosError) => {
this.logger.error("error in sending verify sms", err);
throw new InternalServerErrorException("error in sending verify sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
}
}
//************************************************* */
async sendInvoiceCreationSms(mobile: string, invoiceId: string, price: number, items: string, invoiceDate: string, dueDate: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "invoiceId", value: invoiceId },
{ name: "price", value: Intl.NumberFormat("fa-IR").format(price) },
{ name: "items", value: items },
{ name: "invoiceDate", value: invoiceDate },
{ name: "dueDate", value: dueDate },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_CREATED,
};
try {
const { data } = await firstValueFrom(
this.httpService
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
})
.pipe(
catchError((err: AxiosError) => {
console.error(err);
this.logger.error("error in sending invoice created sms", err);
throw new InternalServerErrorException("error in sending invoice created sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
}
}
//************************************************* */
async sendAnnouncementSms(mobile: string, title: string, description: string, date: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "title", value: title },
{ name: "description", value: description },
{ name: "date", value: date },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_ANNOUNCEMENT,
};
try {
const { data } = await firstValueFrom(
this.httpService
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
})
.pipe(
catchError((err: AxiosError) => {
this.logger.error("error in sending announcement sms", err);
throw new InternalServerErrorException("error in sending announcement sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
}
}
//************************************************* */
async sendCreateTicketSms(mobile: string, ticketId: string, subject: string, date: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "ticketId", value: ticketId },
{ name: "subject", value: subject },
{ name: "date", value: date },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_TICKET_CREATED,
};
try {
const { data } = await firstValueFrom(
this.httpService
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
})
.pipe(
catchError((err: AxiosError) => {
this.logger.error("error in sending ticket created sms", err);
throw new InternalServerErrorException("error in sending ticket created sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
}
}
//************************************************* */
async sendAnswerTicketSms(mobile: string, ticketId: string, subject: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "ticketId", value: ticketId },
{ name: "subject", value: subject },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_TICKET_ANSWERED,
};
try {
const { data } = await firstValueFrom(
this.httpService
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
})
.pipe(
catchError((err: AxiosError) => {
this.logger.error("error in sending ticket answered sms", err);
throw new InternalServerErrorException("error in sending ticket answered sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
}
}
//************************************************* */
async sendTicketAssignedToAdminSms(mobile: string, ticketId: string, subject: string, user: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "ticketId", value: ticketId },
{ name: "subject", value: subject },
{ name: "user", value: user },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_TICKET_ASSIGNED_ADMIN,
};
try {
const { data } = await firstValueFrom(
this.httpService
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
})
.pipe(
catchError((err: AxiosError) => {
this.logger.error("error in sending ticket assigned sms", err);
throw new InternalServerErrorException("error in sending ticket assigned sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
}
}
//************************************************* */
async sendInvoiceApprovedSms(mobile: string, invoiceId: string, price: number, dueDate: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "invoiceId", value: invoiceId },
{ name: "price", value: Intl.NumberFormat("fa-IR").format(price) },
{ name: "dueDate", value: dueDate },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_APPROVED,
};
try {
const { data } = await firstValueFrom(
this.httpService
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
})
.pipe(
catchError((err: AxiosError) => {
this.logger.error("error in sending invoice approved sms", err);
throw new InternalServerErrorException("error in sending invoice approved sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
// throw new InternalServerErrorException("error in sending sms");
}
}
//************************************************* */
async sendInvoicePaidSms(mobile: string, invoiceId: string, amount: number, paidDate: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "invoiceId", value: invoiceId },
{ name: "amount", value: amount.toString() },
{ name: "paidDate", value: paidDate },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_PAID,
};
try {
const { data } = await firstValueFrom(
this.httpService
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
})
.pipe(
catchError((err: AxiosError) => {
this.logger.error("error in sending invoice paid sms", err);
throw new InternalServerErrorException("error in sending invoice paid sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
// throw new InternalServerErrorException("error in sending sms");
}
}
//************************************************* */
async sendInvoicePaymentReminderSms(mobile: string, invoiceId: string, amount: number, dueDate: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "invoiceId", value: invoiceId },
{ name: "amount", value: amount.toString() },
{ name: "dueDate", value: dueDate },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_REMINDER,
};
try {
const { data } = await firstValueFrom(
this.httpService
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
})
.pipe(
catchError((err: AxiosError) => {
this.logger.error("error in sending invoice reminder sms", err);
throw new InternalServerErrorException("error in sending invoice reminder sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
}
}
//************************************************* */
async sendInvoiceOverdueSms(mobile: string, invoiceId: string, amount: number, lateFee: number, dueDate: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "invoiceId", value: invoiceId },
{ name: "amount", value: amount.toString() },
{ name: "lateFee", value: lateFee.toString() },
{ name: "dueDate", value: dueDate },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_OVERDUE,
};
try {
const { data } = await firstValueFrom(
this.httpService
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
})
.pipe(
catchError((err: AxiosError) => {
this.logger.error("error in sending invoice overdue sms", err);
throw new InternalServerErrorException("error in sending invoice overdue sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
}
}
//************************************************* */
async sendRecurringInvoiceDraftSms(adminMobile: string, invoiceId: string, price: number) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "invoiceId", value: invoiceId },
{ name: "price", value: Intl.NumberFormat("fa-IR").format(price) },
],
Mobile: adminMobile,
TemplateId: this.smsConfigs.SMS_PATTERN_RECURRING_INVOICE_DRAFT,
};
try {
const { data } = await firstValueFrom(
this.httpService
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
})
.pipe(
catchError((err: AxiosError) => {
this.logger.error("error in sending recurring invoice draft sms", err);
throw new InternalServerErrorException("error in sending recurring invoice draft sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
// throw new InternalServerErrorException("error in sending sms");
}
}
//************************************************* */
async sendBlockServiceSms(mobile: string, invoiceId: string, planName: string) {
const smsData: ISmsVerifyBody = {
Parameters: [
{ name: "invoiceId", value: invoiceId },
{ name: "planName", value: planName },
],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_SUBSCRIPTION_CANCELLED,
};
try {
const { data } = await firstValueFrom(
this.httpService
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
})
.pipe(
catchError((err: AxiosError) => {
this.logger.error("error in sending subscription cancelled sms", err);
throw new InternalServerErrorException("error in sending subscription cancelled sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
// throw new InternalServerErrorException("error in sending sms");
}
}
//************************************************* */
//************************************************* */
async sendPaymentReminderSms(mobile: string, amount: string) {
const smsData: ISmsVerifyBody = {
Parameters: [{ name: "amount", value: amount }],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_PAYMENT_REMINDER,
};
try {
const { data } = await firstValueFrom(
this.httpService
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
})
.pipe(
catchError((err: AxiosError) => {
this.logger.error("error in sending payment reminder sms", err);
throw new InternalServerErrorException("error in sending payment reminder sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
}
}
//************************************************* */
async sendPaymentCancellationSms(mobile: string, amount: string) {
const smsData: ISmsVerifyBody = {
Parameters: [{ name: "amount", value: amount }],
Mobile: mobile,
TemplateId: this.smsConfigs.SMS_PATTERN_PAYMENT_CANCELLATION,
};
try {
const { data } = await firstValueFrom(
this.httpService
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
})
.pipe(
catchError((err: AxiosError) => {
this.logger.error("error in sending payment cancellation sms", err);
throw new InternalServerErrorException("error in sending payment cancellation sms");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in sending sms", error);
}
}
async getSmsLines() {
try {
const { data } = await firstValueFrom(
this.httpService
.get<ISmsGetLineResponse>(`${this.smsConfigs.API_URL}/lines`, {
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
})
.pipe(
catchError((error: AxiosError) => {
this.logger.error("error in getting sms lines", error);
throw new InternalServerErrorException("error in getting sms lines");
}),
),
);
return data;
} catch (error) {
this.logger.error("error in getting sms lines", error);
}
}
}
+2 -10
View File
@@ -1,30 +1,22 @@
import { Module } from "@nestjs/common";
import { S3_CONFIG, SMS_CONFIG } from "./constants";
import { S3_CONFIG } from "./constants";
import { CacheService } from "./services/cache.service";
import { OTPService } from "./services/otp.service";
import { PasswordService } from "./services/password.service";
import { SmsService } from "./services/sms.service";
import { S3Configs } from "../../configs/s3.config";
import { smsConfigs } from "../../configs/sms.config";
@Module({
providers: [
OTPService,
PasswordService,
CacheService,
SmsService,
{
provide: SMS_CONFIG,
useFactory: smsConfigs().useFactory,
inject: smsConfigs().inject,
},
{
provide: S3_CONFIG,
useFactory: S3Configs().useFactory,
inject: S3Configs().inject,
},
],
exports: [SMS_CONFIG, S3_CONFIG, OTPService, PasswordService, CacheService, SmsService],
exports: [S3_CONFIG, OTPService, PasswordService, CacheService],
})
export class UtilsModule {}