Initial Mento CRM: portal OTP, reports, SMS, and deploy for crm.montzar.ir.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-29 21:06:30 +03:30
co-authored by Cursor
commit 29a040c21d
195 changed files with 39430 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
import {
Injectable,
NotFoundException,
BadRequestException,
} from '@nestjs/common';
import type {
CreateProductRequest,
ProductDto,
UpdateProductRequest,
} from '@mento/shared';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
type ProductRow = {
id: string;
name: string;
sku: string | null;
description: string | null;
unitPrice: Prisma.Decimal | null;
currency: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
};
@Injectable()
export class ProductsService {
constructor(private readonly prisma: PrismaService) {}
async list(
organizationId: string,
opts?: { activeOnly?: boolean; q?: string },
): Promise<ProductDto[]> {
const rows = await this.prisma.product.findMany({
where: {
organizationId,
...(opts?.activeOnly ? { isActive: true } : {}),
...(opts?.q?.trim()
? {
OR: [
{ name: { contains: opts.q.trim(), mode: 'insensitive' } },
{ sku: { contains: opts.q.trim(), mode: 'insensitive' } },
],
}
: {}),
},
orderBy: [{ isActive: 'desc' }, { name: 'asc' }],
});
return rows.map((row) => this.toDto(row));
}
async create(
organizationId: string,
dto: CreateProductRequest,
): Promise<ProductDto> {
const name = dto.name.trim();
if (!name) throw new BadRequestException('نام محصول الزامی است');
const row = await this.prisma.product.create({
data: {
organizationId,
name,
sku: emptyToNull(dto.sku),
description: emptyToNull(dto.description),
unitPrice:
dto.unitPrice == null || Number.isNaN(Number(dto.unitPrice))
? null
: new Prisma.Decimal(dto.unitPrice),
currency: (dto.currency ?? 'IRR').trim() || 'IRR',
isActive: dto.isActive ?? true,
},
});
return this.toDto(row);
}
async update(
organizationId: string,
id: string,
dto: UpdateProductRequest,
): Promise<ProductDto> {
await this.ensureExists(organizationId, id);
const data: Prisma.ProductUpdateInput = {};
if (dto.name !== undefined) {
const name = dto.name.trim();
if (!name) throw new BadRequestException('نام محصول الزامی است');
data.name = name;
}
if ('sku' in dto) data.sku = emptyToNull(dto.sku);
if ('description' in dto) data.description = emptyToNull(dto.description);
if ('unitPrice' in dto) {
data.unitPrice =
dto.unitPrice == null || Number.isNaN(Number(dto.unitPrice))
? null
: new Prisma.Decimal(dto.unitPrice);
}
if (dto.currency !== undefined) {
data.currency = dto.currency.trim() || 'IRR';
}
if (dto.isActive !== undefined) data.isActive = dto.isActive;
const row = await this.prisma.product.update({ where: { id }, data });
return this.toDto(row);
}
async remove(organizationId: string, id: string): Promise<void> {
await this.ensureExists(organizationId, id);
await this.prisma.product.delete({ where: { id } });
}
async ensureInOrg(organizationId: string, productId: string): Promise<void> {
await this.ensureExists(organizationId, productId);
}
private async ensureExists(organizationId: string, id: string) {
const found = await this.prisma.product.findFirst({
where: { id, organizationId },
select: { id: true },
});
if (!found) throw new NotFoundException('محصول پیدا نشد');
}
private toDto(row: ProductRow): ProductDto {
return {
id: row.id,
name: row.name,
sku: row.sku,
description: row.description,
unitPrice: row.unitPrice == null ? null : Number(row.unitPrice),
currency: row.currency,
isActive: row.isActive,
createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(),
};
}
}
function emptyToNull(value?: string | null): string | null {
if (value == null) return null;
const trimmed = value.trim();
return trimmed.length ? trimmed : null;
}