Files
CRM_Montzar/apps/api/src/integrations/tahesab/connections.service.ts
T

314 lines
9.9 KiB
TypeScript

import {
BadRequestException,
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { TahesabCryptoService } from './tahesab-crypto.service';
import { TahesabHttpClient } from './tahesab-http.client';
import { TahesabApi } from './tahesab-methods';
import {
CreateTahesabConnectionDto,
UpdateTahesabConnectionDto,
} from './dto';
function normalizeBaseUrl(url: string): string {
let u = url.trim().replace(/\/+$/, '');
if (!/^https?:\/\//i.test(u)) {
throw new BadRequestException('baseUrl باید با http:// یا https:// شروع شود.');
}
return u;
}
@Injectable()
export class TahesabConnectionsService {
constructor(
private readonly prisma: PrismaService,
private readonly crypto: TahesabCryptoService,
private readonly http: TahesabHttpClient,
) {}
private toDto(row: {
id: string;
organizationId: string;
provider: string;
label: string;
baseUrl: string;
dbName: string;
tokenLast4: string;
enabled: boolean;
isDefault: boolean;
allowInsecureTls: boolean;
lastHealthAt: Date | null;
lastHealthOk: boolean | null;
lastError: string | null;
createdAt: Date;
updatedAt: Date;
}) {
return {
id: row.id,
organizationId: row.organizationId,
provider: row.provider,
label: row.label,
baseUrl: row.baseUrl,
dbName: row.dbName,
tokenMasked: row.tokenLast4 ? `••••••${row.tokenLast4}` : '••••',
enabled: row.enabled,
isDefault: row.isDefault,
allowInsecureTls: row.allowInsecureTls,
lastHealthAt: row.lastHealthAt?.toISOString() ?? null,
lastHealthOk: row.lastHealthOk,
lastError: row.lastError,
createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(),
};
}
async list(organizationId: string) {
const rows = await this.prisma.accountingConnection.findMany({
where: { organizationId, provider: 'tahesab' },
orderBy: [{ isDefault: 'desc' }, { createdAt: 'asc' }],
});
return rows.map((r) => this.toDto(r));
}
async getOrThrow(organizationId: string, id: string) {
const row = await this.prisma.accountingConnection.findFirst({
where: { id, organizationId, provider: 'tahesab' },
});
if (!row) throw new NotFoundException('اتصال ته‌حساب یافت نشد.');
return row;
}
async getApi(organizationId: string, connectionId: string) {
const row = await this.getOrThrow(organizationId, connectionId);
if (!row.enabled) {
throw new BadRequestException('این اتصال غیرفعال است.');
}
const token = this.crypto.decrypt({
ciphertext: row.tokenCiphertext,
iv: row.tokenIv,
tag: row.tokenTag,
});
return {
row,
api: new TahesabApi(this.http, {
baseUrl: row.baseUrl,
token,
dbName: row.dbName,
allowInsecureTls: row.allowInsecureTls,
}),
};
}
async resolveConnectionId(
organizationId: string,
connectionId?: string | null,
) {
if (connectionId) {
await this.getOrThrow(organizationId, connectionId);
return connectionId;
}
const def = await this.prisma.accountingConnection.findFirst({
where: { organizationId, provider: 'tahesab', enabled: true, isDefault: true },
});
if (def) return def.id;
const any = await this.prisma.accountingConnection.findFirst({
where: { organizationId, provider: 'tahesab', enabled: true },
orderBy: { createdAt: 'asc' },
});
if (!any) {
throw new BadRequestException(
'هیچ اتصال فعالی به ته‌حساب تعریف نشده است. از تنظیمات یک اتصال اضافه کنید.',
);
}
return any.id;
}
private async clearOtherDefaults(organizationId: string, exceptId?: string) {
await this.prisma.accountingConnection.updateMany({
where: {
organizationId,
provider: 'tahesab',
...(exceptId ? { id: { not: exceptId } } : {}),
},
data: { isDefault: false },
});
}
async create(organizationId: string, dto: CreateTahesabConnectionDto) {
const baseUrl = normalizeBaseUrl(dto.baseUrl);
const existing = await this.prisma.accountingConnection.findFirst({
where: { organizationId, baseUrl, dbName: dto.dbName.trim() },
});
if (existing) {
throw new ConflictException(
'اتصالی با همین آدرس و نام دیتابیس از قبل وجود دارد.',
);
}
const count = await this.prisma.accountingConnection.count({
where: { organizationId, provider: 'tahesab' },
});
const makeDefault = dto.isDefault === true || count === 0;
if (makeDefault) await this.clearOtherDefaults(organizationId);
const enc = this.crypto.encrypt(dto.token.trim());
const row = await this.prisma.accountingConnection.create({
data: {
organizationId,
provider: 'tahesab',
label: dto.label.trim(),
baseUrl,
dbName: dto.dbName.trim(),
tokenCiphertext: enc.ciphertext,
tokenIv: enc.iv,
tokenTag: enc.tag,
tokenLast4: this.crypto.last4(dto.token),
enabled: dto.enabled ?? true,
isDefault: makeDefault,
allowInsecureTls: dto.allowInsecureTls ?? false,
},
});
return this.toDto(row);
}
async update(
organizationId: string,
id: string,
dto: UpdateTahesabConnectionDto,
) {
const row = await this.getOrThrow(organizationId, id);
const baseUrl = dto.baseUrl ? normalizeBaseUrl(dto.baseUrl) : row.baseUrl;
const dbName = dto.dbName?.trim() ?? row.dbName;
if (baseUrl !== row.baseUrl || dbName !== row.dbName) {
const clash = await this.prisma.accountingConnection.findFirst({
where: {
organizationId,
baseUrl,
dbName,
id: { not: id },
},
});
if (clash) {
throw new ConflictException(
'اتصالی با همین آدرس و نام دیتابیس از قبل وجود دارد.',
);
}
}
if (dto.isDefault === true) await this.clearOtherDefaults(organizationId, id);
let enc = {
ciphertext: row.tokenCiphertext,
iv: row.tokenIv,
tag: row.tokenTag,
};
let tokenLast4 = row.tokenLast4;
if (dto.token?.trim()) {
enc = this.crypto.encrypt(dto.token.trim());
tokenLast4 = this.crypto.last4(dto.token);
}
const updated = await this.prisma.accountingConnection.update({
where: { id },
data: {
label: dto.label?.trim() ?? row.label,
baseUrl,
dbName,
tokenCiphertext: enc.ciphertext,
tokenIv: enc.iv,
tokenTag: enc.tag,
tokenLast4,
enabled: dto.enabled ?? row.enabled,
isDefault: dto.isDefault === true ? true : dto.isDefault === false ? false : row.isDefault,
allowInsecureTls: dto.allowInsecureTls ?? row.allowInsecureTls,
},
});
return this.toDto(updated);
}
async remove(organizationId: string, id: string) {
const row = await this.getOrThrow(organizationId, id);
await this.prisma.accountingConnection.delete({ where: { id: row.id } });
if (row.isDefault) {
const next = await this.prisma.accountingConnection.findFirst({
where: { organizationId, provider: 'tahesab', enabled: true },
orderBy: { createdAt: 'asc' },
});
if (next) {
await this.prisma.accountingConnection.update({
where: { id: next.id },
data: { isDefault: true },
});
}
}
return { ok: true };
}
async test(organizationId: string, id: string) {
const { row, api } = await this.getApi(organizationId, id);
try {
const result = await api.checkHealth();
const patch: {
lastHealthAt: Date;
lastHealthOk: boolean;
lastError: string | null;
baseUrl?: string;
allowInsecureTls?: boolean;
} = {
lastHealthAt: new Date(),
lastHealthOk: true,
lastError: null,
};
// اگر health فقط با آدرس/پروتکل جایگزین جواب داد، همان را ذخیره کن
if (
result.usedBaseUrl &&
result.usedBaseUrl.replace(/\/+$/, '') !== row.baseUrl.replace(/\/+$/, '')
) {
patch.baseUrl = result.usedBaseUrl.replace(/\/+$/, '');
}
// اگر فقط با TLS بدون verify جواب داد، فلگ را روشن کن تا بقیهٔ متدها هم کار کنند
if (!row.allowInsecureTls && result.usedInsecureTls) {
patch.allowInsecureTls = true;
}
const updated = await this.prisma.accountingConnection.update({
where: { id: row.id },
data: patch,
});
return {
ok: true,
connection: this.toDto(updated),
health: result,
adjusted:
Boolean(patch.baseUrl) || Boolean(patch.allowInsecureTls)
? {
baseUrl: patch.baseUrl,
allowInsecureTls: patch.allowInsecureTls,
}
: undefined,
};
} catch (err) {
const message = (err as Error).message ?? 'خطای ناشناخته';
const hint =
/مهلت اتصال|timeout/i.test(message)
? ' از سرور CRM به IP/پورت ته‌حساب دسترسی شبکه لازم است (فایروال). برای https خودامضا گزینه SSL را فعال کنید. پورت پیش‌فرض: https 8081 یا http 9550.'
: '';
const full = `${message}${hint}`.slice(0, 500);
const updated = await this.prisma.accountingConnection.update({
where: { id: row.id },
data: {
lastHealthAt: new Date(),
lastHealthOk: false,
lastError: full,
},
});
return { ok: false, connection: this.toDto(updated), error: full };
}
}
}