fix(lint): clean up type definitions and missing imports
Some checks failed
Deploy Canina / deploy (push) Failing after 30m46s

This commit is contained in:
parsa aghaei 2026-07-29 16:44:53 +03:30
parent 9c8286c63f
commit 2de442dbae
9 changed files with 147 additions and 62 deletions

View File

@ -1,4 +1,4 @@
import { Injectable, HttpException } from '@nestjs/common'; import { Injectable, HttpException, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { RedisService } from '../redis/redis.service'; import { RedisService } from '../redis/redis.service';

View File

@ -27,7 +27,10 @@ export class AuthService {
// Dispatch OTP via MeliPayamak Pattern SMS // Dispatch OTP via MeliPayamak Pattern SMS
await this.smsService.sendOtp(phoneNumber, code); await this.smsService.sendOtp(phoneNumber, code);
return { success: true, message: 'کد تایید با موفقیت به شماره شما پیامک شد.' }; return {
success: true,
message: 'کد تایید با موفقیت به شماره شما پیامک شد.',
};
} }
async verifyOtp(verifyOtpDto: VerifyOtpDto) { async verifyOtp(verifyOtpDto: VerifyOtpDto) {

View File

@ -19,9 +19,10 @@ export class CustomHttpExceptionFilter implements ExceptionFilter {
const request = ctx.getRequest<Request>(); const request = ctx.getRequest<Request>();
let status = HttpStatus.INTERNAL_SERVER_ERROR; let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'خطای داخلی سرور رخ داده است. لطفاً بعداً تلاش کنید یا با پشتیبانی تماس بگیرید.'; let message =
'خطای داخلی سرور رخ داده است. لطفاً بعداً تلاش کنید یا با پشتیبانی تماس بگیرید.';
let code = 'INTERNAL_SERVER_ERROR'; let code = 'INTERNAL_SERVER_ERROR';
let details: ErrorDetailField[] | Record<string, any> = {}; let details: ErrorDetailField[] | Record<string, unknown> = {};
if (exception instanceof HttpException) { if (exception instanceof HttpException) {
status = exception.getStatus(); status = exception.getStatus();
@ -30,14 +31,17 @@ export class CustomHttpExceptionFilter implements ExceptionFilter {
if (typeof exceptionResponse === 'string') { if (typeof exceptionResponse === 'string') {
message = this.translateGenericMessage(exceptionResponse, status); message = this.translateGenericMessage(exceptionResponse, status);
code = this.deriveErrorCode(status, exceptionResponse); code = this.deriveErrorCode(status, exceptionResponse);
} else if (typeof exceptionResponse === 'object' && exceptionResponse !== null) { } else if (
const resObj = exceptionResponse as Record<string, any>; typeof exceptionResponse === 'object' &&
exceptionResponse !== null
) {
const resObj = exceptionResponse as Record<string, unknown>;
// Handle class-validator validation array messages // Handle class-validator validation array messages
if (Array.isArray(resObj.message)) { if (Array.isArray(resObj.message)) {
message = 'اطلاعات وارد شده نامعتبر است. لطفاً فرم را بررسی کنید.'; message = 'اطلاعات وارد شده نامعتبر است. لطفاً فرم را بررسی کنید.';
code = 'INVALID_INPUT_DATA'; code = 'INVALID_INPUT_DATA';
details = resObj.message.map((msg: string) => { details = (resObj.message as string[]).map((msg: string) => {
const parts = msg.split(' '); const parts = msg.split(' ');
return { return {
field: parts[0] || 'general', field: parts[0] || 'general',
@ -45,13 +49,19 @@ export class CustomHttpExceptionFilter implements ExceptionFilter {
}; };
}); });
} else { } else {
message = this.translateGenericMessage(resObj.message || exception.message, status); const rawMsg = typeof resObj.message === 'string' ? resObj.message : exception.message;
code = resObj.code || this.deriveErrorCode(status, resObj.error); message = this.translateGenericMessage(rawMsg, status);
details = resObj.details || {}; const rawCode = typeof resObj.code === 'string' ? resObj.code : undefined;
const rawError = typeof resObj.error === 'string' ? resObj.error : undefined;
code = rawCode || this.deriveErrorCode(status, rawError);
details = (resObj.details as Record<string, unknown>) || {};
} }
} }
} else if (exception instanceof Error) { } else if (exception instanceof Error) {
this.logger.error(`Unhandled Exception: ${exception.message}`, exception.stack); this.logger.error(
`Unhandled Exception: ${exception.message}`,
exception.stack,
);
message = 'خطای غیرمنتظره در پردازش درخواست رخ داد.'; message = 'خطای غیرمنتظره در پردازش درخواست رخ داد.';
code = 'UNHANDLED_EXCEPTION'; code = 'UNHANDLED_EXCEPTION';
} }
@ -66,14 +76,16 @@ export class CustomHttpExceptionFilter implements ExceptionFilter {
path: request.url, path: request.url,
}; };
this.logger.warn(`[${request.method}] ${request.url} - Status ${status} - ${message}`); this.logger.warn(
`[${request.method}] ${request.url} - Status ${status} - ${message}`,
);
response.status(status).json(errorPayload); response.status(status).json(errorPayload);
} }
private translateGenericMessage(msg: string, status: number): string { private translateGenericMessage(msg: string, status: number): string {
if (!msg) return 'خطای نامشخص رخ داده است.'; if (!msg) return 'خطای نامشخص رخ داده است.';
const lower = msg.toLowerCase(); const lower = msg.toLowerCase();
if (lower.includes('unauthorized') || status === 401) { if (lower.includes('unauthorized') || status === 401) {
return 'شما دسترسی لازم برای این عملیات را ندارید. لطفاً ابتدا وارد حساب کاربری خود شوید.'; return 'شما دسترسی لازم برای این عملیات را ندارید. لطفاً ابتدا وارد حساب کاربری خود شوید.';
@ -100,14 +112,22 @@ export class CustomHttpExceptionFilter implements ExceptionFilter {
} }
switch (status) { switch (status) {
case 400: return 'BAD_REQUEST'; case 400:
case 401: return 'UNAUTHORIZED'; return 'BAD_REQUEST';
case 403: return 'FORBIDDEN'; case 401:
case 404: return 'NOT_FOUND'; return 'UNAUTHORIZED';
case 409: return 'CONFLICT'; case 403:
case 422: return 'UNPROCESSABLE_ENTITY'; return 'FORBIDDEN';
case 429: return 'TOO_MANY_REQUESTS'; case 404:
default: return 'INTERNAL_SERVER_ERROR'; return 'NOT_FOUND';
case 409:
return 'CONFLICT';
case 422:
return 'UNPROCESSABLE_ENTITY';
case 429:
return 'TOO_MANY_REQUESTS';
default:
return 'INTERNAL_SERVER_ERROR';
} }
} }
} }

View File

@ -20,7 +20,7 @@ export class PrismaExceptionFilter implements ExceptionFilter {
let status = HttpStatus.BAD_REQUEST; let status = HttpStatus.BAD_REQUEST;
let message = 'خطایی در پردازش اطلاعات دیتابیس رخ داده است.'; let message = 'خطایی در پردازش اطلاعات دیتابیس رخ داده است.';
let code = `PRISMA_${exception.code}`; let code = `PRISMA_${exception.code}`;
let details: Record<string, any> = {}; let details: Record<string, unknown> = {};
switch (exception.code) { switch (exception.code) {
case 'P2002': { case 'P2002': {
@ -34,14 +34,16 @@ export class PrismaExceptionFilter implements ExceptionFilter {
} }
case 'P2025': { case 'P2025': {
status = HttpStatus.NOT_FOUND; status = HttpStatus.NOT_FOUND;
message = 'اطلاعات یا رکورد مورد نظر در سیستم یافت نشد یا قبلاً حذف شده است.'; message =
'اطلاعات یا رکورد مورد نظر در سیستم یافت نشد یا قبلاً حذف شده است.';
code = 'RECORD_NOT_FOUND'; code = 'RECORD_NOT_FOUND';
details = { meta: exception.meta }; details = { meta: exception.meta };
break; break;
} }
case 'P2003': { case 'P2003': {
status = HttpStatus.BAD_REQUEST; status = HttpStatus.BAD_REQUEST;
message = 'امکان انجام عملیات به دلیل وجود وابستگی بین اطلاعات وجود ندارد.'; message =
'امکان انجام عملیات به دلیل وجود وابستگی بین اطلاعات وجود ندارد.';
code = 'FOREIGN_KEY_CONSTRAINT_FAILED'; code = 'FOREIGN_KEY_CONSTRAINT_FAILED';
break; break;
} }
@ -52,7 +54,9 @@ export class PrismaExceptionFilter implements ExceptionFilter {
} }
} }
this.logger.error(`[Prisma ${exception.code}] ${request.method} ${request.url} - ${message}`); this.logger.error(
`[Prisma ${exception.code}] ${request.method} ${request.url} - ${message}`,
);
response.status(status).json({ response.status(status).json({
success: false, success: false,

View File

@ -4,12 +4,18 @@ export class ErrorDetailField {
@ApiProperty({ description: 'نام فیلد دارای خطا', example: 'phone' }) @ApiProperty({ description: 'نام فیلد دارای خطا', example: 'phone' })
field: string; field: string;
@ApiProperty({ description: 'توضیح توصیفی خطا به فارسی', example: 'شماره تلفن همراه وارد شده نامعتبر است.' }) @ApiProperty({
description: 'توضیح توصیفی خطا به فارسی',
example: 'شماره تلفن همراه وارد شده نامعتبر است.',
})
message: string; message: string;
} }
export class ApiErrorResponse { export class ApiErrorResponse {
@ApiProperty({ description: 'وضعیت موفقیت درخواست (همیشه false در زمان خطا)', example: false }) @ApiProperty({
description: 'وضعیت موفقیت درخواست (همیشه false در زمان خطا)',
example: false,
})
success: boolean; success: boolean;
@ApiProperty({ description: 'کد وضعیت HTTP درخواست', example: 400 }) @ApiProperty({ description: 'کد وضعیت HTTP درخواست', example: 400 })
@ -17,11 +23,15 @@ export class ApiErrorResponse {
@ApiProperty({ @ApiProperty({
description: 'پیام جامع و توصیفی خطا به زبان فارسی', description: 'پیام جامع و توصیفی خطا به زبان فارسی',
example: 'اطلاعات وارد شده نامعتبر است. لطفاً موارد مشخص شده را بررسی نمایید.', example:
'اطلاعات وارد شده نامعتبر است. لطفاً موارد مشخص شده را بررسی نمایید.',
}) })
message: string; message: string;
@ApiProperty({ description: 'کد سیستمیک و ثابت خطا جهت پردازش فرانت‌اند', example: 'INVALID_INPUT_DATA' }) @ApiProperty({
description: 'کد سیستمیک و ثابت خطا جهت پردازش فرانت‌اند',
example: 'INVALID_INPUT_DATA',
})
code: string; code: string;
@ApiProperty({ @ApiProperty({
@ -31,9 +41,15 @@ export class ApiErrorResponse {
}) })
details?: ErrorDetailField[] | Record<string, any>; details?: ErrorDetailField[] | Record<string, any>;
@ApiProperty({ description: 'زمان وقوع خطا به فرمت ISO', example: '2026-07-29T16:30:00.000Z' }) @ApiProperty({
description: 'زمان وقوع خطا به فرمت ISO',
example: '2026-07-29T16:30:00.000Z',
})
timestamp: string; timestamp: string;
@ApiProperty({ description: 'مسیر و انکوینتی که خطا در آن رخ داده است', example: '/api/auth/register' }) @ApiProperty({
description: 'مسیر و انکوینتی که خطا در آن رخ داده است',
example: '/api/auth/register',
})
path: string; path: string;
} }

View File

@ -68,7 +68,9 @@ export class SmsService {
); );
req.on('error', (err) => { req.on('error', (err) => {
this.logger.error(`[SMS Exception] MeliPayamak HTTP Error: ${err.message}`); this.logger.error(
`[SMS Exception] MeliPayamak HTTP Error: ${err.message}`,
);
resolve(false); resolve(false);
}); });
@ -92,7 +94,11 @@ export class SmsService {
/** /**
* Send Order Confirmation SMS * Send Order Confirmation SMS
*/ */
async sendOrderConfirmation(phone: string, orderNumber: string, amount: string): Promise<boolean> { async sendOrderConfirmation(
phone: string,
orderNumber: string,
amount: string,
): Promise<boolean> {
const bodyId = parseInt(process.env.MELIPAYAMAK_ORDER_BODY_ID || '0', 10); const bodyId = parseInt(process.env.MELIPAYAMAK_ORDER_BODY_ID || '0', 10);
return this.sendPatternSms({ return this.sendPatternSms({
to: phone, to: phone,
@ -104,8 +110,15 @@ export class SmsService {
/** /**
* Send Shipping Status SMS with Tracking Code * Send Shipping Status SMS with Tracking Code
*/ */
async sendShippingNotification(phone: string, orderNumber: string, trackingCode: string): Promise<boolean> { async sendShippingNotification(
const bodyId = parseInt(process.env.MELIPAYAMAK_SHIPPING_BODY_ID || '0', 10); phone: string,
orderNumber: string,
trackingCode: string,
): Promise<boolean> {
const bodyId = parseInt(
process.env.MELIPAYAMAK_SHIPPING_BODY_ID || '0',
10,
);
return this.sendPatternSms({ return this.sendPatternSms({
to: phone, to: phone,
bodyId, bodyId,
@ -116,8 +129,15 @@ export class SmsService {
/** /**
* Send Pet Care Vaccination / Deworming Reminder SMS * Send Pet Care Vaccination / Deworming Reminder SMS
*/ */
async sendPetCareReminder(phone: string, petName: string, reminderType: string): Promise<boolean> { async sendPetCareReminder(
const bodyId = parseInt(process.env.MELIPAYAMAK_PET_CARE_BODY_ID || '0', 10); phone: string,
petName: string,
reminderType: string,
): Promise<boolean> {
const bodyId = parseInt(
process.env.MELIPAYAMAK_PET_CARE_BODY_ID || '0',
10,
);
return this.sendPatternSms({ return this.sendPatternSms({
to: phone, to: phone,
bodyId, bodyId,

View File

@ -36,15 +36,21 @@ async function bootstrap() {
forbidNonWhitelisted: true, forbidNonWhitelisted: true,
exceptionFactory: (errors) => { exceptionFactory: (errors) => {
const formattedDetails = errors.map((err) => { const formattedDetails = errors.map((err) => {
const constraints = err.constraints ? Object.values(err.constraints) : []; const constraints = err.constraints
? Object.values(err.constraints)
: [];
return { return {
field: err.property, field: err.property,
message: constraints.length > 0 ? constraints[0] : 'مقدار وارد شده نامعتبر است.', message:
constraints.length > 0
? constraints[0]
: 'مقدار وارد شده نامعتبر است.',
}; };
}); });
return new BadRequestException({ return new BadRequestException({
message: 'اطلاعات ورودی فرم معتبر نمی‌باشد. لطفاً موارد مشخص شده را اصلاح نمایید.', message:
'اطلاعات ورودی فرم معتبر نمی‌باشد. لطفاً موارد مشخص شده را اصلاح نمایید.',
error: 'INVALID_INPUT_DATA', error: 'INVALID_INPUT_DATA',
details: formattedDetails, details: formattedDetails,
}); });
@ -52,7 +58,10 @@ async function bootstrap() {
}), }),
); );
app.useGlobalFilters(new CustomHttpExceptionFilter(), new PrismaExceptionFilter()); app.useGlobalFilters(
new CustomHttpExceptionFilter(),
new PrismaExceptionFilter(),
);
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle('Canina Iran API') .setTitle('Canina Iran API')

View File

@ -92,7 +92,12 @@ export class OrdersService {
// Verify stock and build items // Verify stock and build items
let cartTotal = 0; let cartTotal = 0;
const orderItems: Array<{ productId: string; quantity: number; unitPrice: number; totalPrice: number }> = []; const orderItems: Array<{
productId: string;
quantity: number;
unitPrice: number;
totalPrice: number;
}> = [];
for (const item of createOrderDto.items) { for (const item of createOrderDto.items) {
const product = await this.prisma.product.findUnique({ const product = await this.prisma.product.findUnique({
@ -182,15 +187,20 @@ export class OrdersService {
// Send Order Confirmation SMS // Send Order Confirmation SMS
if (userId) { if (userId) {
this.prisma.user.findUnique({ where: { id: userId } }).then((user) => { this.prisma.user
if (user && user.mobile) { .findUnique({ where: { id: userId } })
this.smsService.sendOrderConfirmation( .then((user) => {
user.mobile, if (user && user.mobile) {
trackingNumber, this.smsService
finalAmount.toLocaleString('fa-IR'), .sendOrderConfirmation(
).catch(() => {}); user.mobile,
} trackingNumber,
}).catch(() => {}); finalAmount.toLocaleString('fa-IR'),
)
.catch(() => {});
}
})
.catch(() => {});
} }
return createdOrder; return createdOrder;
@ -219,11 +229,9 @@ export class OrdersService {
const mobile = updatedOrder.user?.mobile; const mobile = updatedOrder.user?.mobile;
const trackingNum = updatedOrder.trackingNumber || trackingCode || ''; const trackingNum = updatedOrder.trackingNumber || trackingCode || '';
if (status === 'shipped' && mobile && trackingCode) { if (status === 'shipped' && mobile && trackingCode) {
this.smsService.sendShippingNotification( this.smsService
mobile, .sendShippingNotification(mobile, trackingNum, trackingCode)
trackingNum, .catch(() => {});
trackingCode,
).catch(() => {});
} }
return updatedOrder; return updatedOrder;

View File

@ -26,11 +26,16 @@ export class WholesaleService {
// Alert admin via SMS if admin mobile is configured // Alert admin via SMS if admin mobile is configured
const adminMobile = process.env.ADMIN_MOBILE_ALERT; const adminMobile = process.env.ADMIN_MOBILE_ALERT;
if (adminMobile) { if (adminMobile) {
this.smsService.sendPatternSms({ this.smsService
to: adminMobile, .sendPatternSms({
bodyId: parseInt(process.env.MELIPAYAMAK_ADMIN_ALERT_BODY_ID || '0', 10), to: adminMobile,
args: [dto.businessName || 'نامشخص', user.mobile || ''], bodyId: parseInt(
}).catch(() => {}); process.env.MELIPAYAMAK_ADMIN_ALERT_BODY_ID || '0',
10,
),
args: [dto.businessName || 'نامشخص', user.mobile || ''],
})
.catch(() => {});
} }
return { return {