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:
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
Min,
|
||||
MinLength,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class ExchangeRateInputDto {
|
||||
@IsString()
|
||||
@MinLength(3)
|
||||
@MaxLength(8)
|
||||
currency!: string;
|
||||
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(0.000001)
|
||||
rateToBase!: number;
|
||||
}
|
||||
|
||||
export class UpdateOrgCurrenciesDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(3)
|
||||
@MaxLength(8)
|
||||
baseCurrency?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@IsString({ each: true })
|
||||
enabledCurrencies?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ExchangeRateInputDto)
|
||||
rates?: ExchangeRateInputDto[];
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Body, Controller, Get, Patch, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/roles.guard';
|
||||
import { Roles } from '../auth/roles.decorator';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { UpdateOrgCurrenciesDto } from './dto';
|
||||
import { OrganizationsService } from './organizations.service';
|
||||
|
||||
@Controller('organizations')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
export class OrganizationsController {
|
||||
constructor(private readonly organizationsService: OrganizationsService) {}
|
||||
|
||||
@Get('current')
|
||||
current(@CurrentUser() user: { organizationId: string }) {
|
||||
return this.organizationsService.getCurrent(user.organizationId);
|
||||
}
|
||||
|
||||
@Get('currency-catalog')
|
||||
catalog() {
|
||||
return this.organizationsService.catalog();
|
||||
}
|
||||
|
||||
@Patch('current/currencies')
|
||||
@Roles('owner', 'admin')
|
||||
updateCurrencies(
|
||||
@CurrentUser() user: { organizationId: string },
|
||||
@Body() dto: UpdateOrgCurrenciesDto,
|
||||
) {
|
||||
return this.organizationsService.updateCurrencies(
|
||||
user.organizationId,
|
||||
dto,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { OrganizationsController } from './organizations.controller';
|
||||
import { OrganizationsService } from './organizations.service';
|
||||
|
||||
@Module({
|
||||
controllers: [OrganizationsController],
|
||||
providers: [OrganizationsService],
|
||||
})
|
||||
export class OrganizationsModule {}
|
||||
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import type {
|
||||
OrganizationDto,
|
||||
UpdateOrgCurrenciesRequest,
|
||||
} from '@mento/shared';
|
||||
import { CURRENCY_CATALOG } from '@mento/shared';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { isKnownCurrency } from '../common/currency';
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async getCurrent(organizationId: string): Promise<OrganizationDto> {
|
||||
const org = await this.prisma.organization.findUnique({
|
||||
where: { id: organizationId },
|
||||
include: {
|
||||
exchangeRates: { orderBy: { currency: 'asc' } },
|
||||
},
|
||||
});
|
||||
if (!org) {
|
||||
throw new NotFoundException('سازمان پیدا نشد');
|
||||
}
|
||||
return this.toDto(org);
|
||||
}
|
||||
|
||||
async updateCurrencies(
|
||||
organizationId: string,
|
||||
dto: UpdateOrgCurrenciesRequest,
|
||||
): Promise<OrganizationDto> {
|
||||
const org = await this.prisma.organization.findUnique({
|
||||
where: { id: organizationId },
|
||||
});
|
||||
if (!org) throw new NotFoundException('سازمان پیدا نشد');
|
||||
|
||||
const baseCurrency = (
|
||||
dto.baseCurrency?.trim() ||
|
||||
org.baseCurrency ||
|
||||
'IRR'
|
||||
).toUpperCase();
|
||||
if (!isKnownCurrency(baseCurrency)) {
|
||||
throw new BadRequestException('ارز پایه معتبر نیست');
|
||||
}
|
||||
|
||||
let enabled = (
|
||||
dto.enabledCurrencies?.map((c) => c.trim().toUpperCase()) ??
|
||||
org.enabledCurrencies
|
||||
).filter(Boolean);
|
||||
if (!enabled.includes(baseCurrency)) {
|
||||
enabled = [baseCurrency, ...enabled];
|
||||
}
|
||||
enabled = [...new Set(enabled)];
|
||||
for (const code of enabled) {
|
||||
if (!isKnownCurrency(code)) {
|
||||
throw new BadRequestException(`ارز ${code} پشتیبانی نمیشود`);
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.rates) {
|
||||
for (const row of dto.rates) {
|
||||
const code = row.currency.trim().toUpperCase();
|
||||
if (!isKnownCurrency(code)) {
|
||||
throw new BadRequestException(`ارز ${code} پشتیبانی نمیشود`);
|
||||
}
|
||||
if (code === baseCurrency) continue;
|
||||
if (row.rateToBase <= 0) {
|
||||
throw new BadRequestException('نرخ باید بزرگتر از صفر باشد');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.organization.update({
|
||||
where: { id: organizationId },
|
||||
data: {
|
||||
baseCurrency,
|
||||
enabledCurrencies: enabled,
|
||||
},
|
||||
});
|
||||
|
||||
if (dto.rates) {
|
||||
for (const row of dto.rates) {
|
||||
const code = row.currency.trim().toUpperCase();
|
||||
if (code === baseCurrency) {
|
||||
await tx.exchangeRate.deleteMany({
|
||||
where: { organizationId, currency: code },
|
||||
});
|
||||
continue;
|
||||
}
|
||||
await tx.exchangeRate.upsert({
|
||||
where: {
|
||||
organizationId_currency: { organizationId, currency: code },
|
||||
},
|
||||
create: {
|
||||
organizationId,
|
||||
currency: code,
|
||||
rateToBase: new Prisma.Decimal(row.rateToBase),
|
||||
},
|
||||
update: {
|
||||
rateToBase: new Prisma.Decimal(row.rateToBase),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await tx.exchangeRate.deleteMany({
|
||||
where: {
|
||||
organizationId,
|
||||
OR: [
|
||||
{ currency: baseCurrency },
|
||||
{ currency: { notIn: enabled } },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return this.getCurrent(organizationId);
|
||||
}
|
||||
|
||||
catalog() {
|
||||
return CURRENCY_CATALOG;
|
||||
}
|
||||
|
||||
private toDto(org: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
baseCurrency: string;
|
||||
enabledCurrencies: string[];
|
||||
exchangeRates: Array<{
|
||||
currency: string;
|
||||
rateToBase: Prisma.Decimal;
|
||||
updatedAt: Date;
|
||||
}>;
|
||||
}): OrganizationDto {
|
||||
return {
|
||||
id: org.id,
|
||||
name: org.name,
|
||||
slug: org.slug,
|
||||
baseCurrency: org.baseCurrency,
|
||||
enabledCurrencies: org.enabledCurrencies,
|
||||
exchangeRates: org.exchangeRates.map((r) => ({
|
||||
currency: r.currency,
|
||||
rateToBase: Number(r.rateToBase),
|
||||
updatedAt: r.updatedAt.toISOString(),
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user