import axios, { AxiosRequestConfig } from "axios";
import { CreateEmployeeDTO, EmployeeSearch, SendInviteDTO, UpdateEmployeeDTO } from "../schemas/employee-schemas.js";
import { ErrorHandler } from "../utils/error-handler.js";
import { EmployeeApi, EmployeeSearchParams } from "./generated/index.js";
/**
* API клиент для работы с employee endpoints
* Интегрируется с существующей системой аутентификации
*/
export class EmployeeApiClient {
private baseUrl: string;
private apiKey: string;
private generatedApi: EmployeeApi;
constructor(baseUrl?: string, apiKey?: string) {
this.baseUrl = baseUrl || process.env.EMPLOYEE_API_HOST_URL || "https://api.development.myradius.ru";
this.apiKey = apiKey || process.env.EMPLOYEE_API_KEY || "";
// Добавляем /employees prefix если не присутствует
if (!this.baseUrl.includes("/employees")) {
this.baseUrl = this.baseUrl.replace(/\/$/, "") + "/employees/";
}
// Инициализируем сгенерированный API клиент
this.generatedApi = new EmployeeApi(this.baseUrl, this.apiKey);
}
/**
* Выполнение HTTP запроса
*/
private async makeRequest<T>(method: string, path: string, body: unknown = null, params: unknown = null): Promise<T> {
const url = `${this.baseUrl}${path}`;
const headers: Record<string, string> = {
Authorization: this.apiKey,
Accept: "application/json",
};
if (method.toUpperCase() !== "GET" && body !== null) {
headers["Content-Type"] = "application/json";
}
console.log("[DEBUG] Employee API Request", {
method,
url,
headers: { ...headers, Authorization: "***" },
body,
params,
});
try {
const config: AxiosRequestConfig = {
url,
method,
headers,
params,
};
if (method.toUpperCase() !== "GET" && body !== null) {
config.data = body;
}
const response = await axios(config);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response) {
console.error("[DEBUG] Employee API Error Response", {
status: error.response.status,
data: error.response.data,
});
}
throw new Error(`Employee API request failed: ${error.message}`);
}
throw error;
}
}
/**
* Создание сотрудника
* POST /employees
*/
async createEmployee(employee: CreateEmployeeDTO): Promise<unknown> {
const context = ErrorHandler.createErrorContext(
"createEmployee",
`${this.baseUrl}/employees`,
"POST",
undefined,
employee.companyId
);
return ErrorHandler.executeWithResilience(async () => {
return this.generatedApi.createEmployee(employee);
}, context);
}
/**
* Поиск сотрудников
* GET /employees/search
*/
async searchEmployees(searchParams: EmployeeSearch): Promise<unknown> {
const params: EmployeeSearchParams = {
lastName: searchParams.lastName,
firstName: searchParams.firstName,
email: searchParams.email,
};
const context = ErrorHandler.createErrorContext("searchEmployees", `${this.baseUrl}/employees/search`, "GET");
return ErrorHandler.executeWithResilience(async () => {
return this.generatedApi.searchEmployees(params);
}, context);
}
/**
* Получение сотрудника по ID
* GET /employees/{id}
*/
async getEmployee(id: string): Promise<unknown> {
return this.generatedApi.getEmployee(id);
}
/**
* Удаление сотрудника
* DELETE /employees/{id}
*/
async deleteEmployee(id: string): Promise<unknown> {
return this.generatedApi.deleteEmployee(id);
}
/**
* Отправка приглашения
* POST /invites/send
*/
async sendInvite(invite: SendInviteDTO): Promise<unknown> {
// Для invites может быть другой endpoint
const inviteUrl = this.baseUrl.replace("/employees/", "/invites/");
const url = `${inviteUrl}send`;
const headers: Record<string, string> = {
Authorization: this.apiKey,
Accept: "application/json",
"Content-Type": "application/json",
};
console.log("[DEBUG] Invite API Request", {
method: "POST",
url,
headers: { ...headers, Authorization: "***" },
body: invite,
});
try {
const response = await axios.post(url, invite, { headers });
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response) {
console.error("[DEBUG] Invite API Error Response", {
status: error.response.status,
data: error.response.data,
});
}
throw new Error(`Invite API request failed: ${error.message}`);
}
throw error;
}
}
/**
* Проверка уникальности сотрудника
* Использует search для проверки существования
*/
async checkEmployeeUniqueness(
lastName: string,
firstName: string,
email?: string
): Promise<{
found: boolean;
employees: unknown[];
message?: string;
exactMatch?: boolean;
similarMatches?: unknown[];
conflictType?: "exact" | "similar" | "email" | "none";
}> {
try {
// Поиск по точному совпадению ФИО
const exactSearchParams: EmployeeSearch = {
lastName,
firstName,
...(email && { email }),
};
const exactResult = await this.searchEmployees(exactSearchParams);
const exactEmployees = Array.isArray(exactResult)
? exactResult
: (exactResult as { employees?: unknown[] })?.employees || [];
// Если найдено точное совпадение
if (exactEmployees.length > 0) {
return {
found: true,
employees: exactEmployees,
exactMatch: true,
conflictType: "exact",
message: `Найдено точное совпадение: ${exactEmployees.length} сотрудник(ов) с такими ФИО${email ? " и email" : ""}`,
};
}
// Поиск по email (если указан)
if (email) {
const emailSearchParams: EmployeeSearch = { email };
const emailResult = await this.searchEmployees(emailSearchParams);
const emailEmployees = Array.isArray(emailResult)
? emailResult
: (emailResult as { employees?: unknown[] })?.employees || [];
if (emailEmployees.length > 0) {
return {
found: true,
employees: emailEmployees,
exactMatch: false,
similarMatches: emailEmployees,
conflictType: "email",
message: `Найден сотрудник с таким же email: ${email}`,
};
}
}
// Поиск похожих сотрудников (только по фамилии)
const similarSearchParams: EmployeeSearch = { lastName };
const similarResult = await this.searchEmployees(similarSearchParams);
const similarEmployees = Array.isArray(similarResult)
? similarResult
: (similarResult as { employees?: unknown[] })?.employees || [];
if (similarEmployees.length > 0) {
return {
found: false,
employees: [],
exactMatch: false,
similarMatches: similarEmployees,
conflictType: "similar",
message: `Найдено ${similarEmployees.length} сотрудник(ов) с такой же фамилией. Проверьте, не является ли это дубликатом.`,
};
}
return {
found: false,
employees: [],
exactMatch: false,
conflictType: "none",
message: "Сотрудник не найден, можно создавать",
};
} catch (error) {
console.error("[ERROR] Failed to check employee uniqueness", error);
return {
found: false,
employees: [],
message: "Ошибка при проверке уникальности",
conflictType: "none",
};
}
}
/**
* Обработка конфликта при создании сотрудника
* Предлагает варианты разрешения конфликта
*/
async handleEmployeeConflict(
createDto: CreateEmployeeDTO,
conflictInfo: {
found: boolean;
employees: unknown[];
conflictType?: "exact" | "similar" | "email" | "none";
message?: string;
}
): Promise<{
action: "create" | "update" | "skip" | "manual";
message: string;
suggestedActions?: string[];
}> {
if (!conflictInfo.found || conflictInfo.conflictType === "none") {
return {
action: "create",
message: "Конфликтов не найдено, создаём нового сотрудника",
};
}
switch (conflictInfo.conflictType) {
case "exact":
return {
action: "manual",
message: `⚠️ Найден точный дубликат: ${conflictInfo.message}`,
suggestedActions: [
"1. Обновить существующую запись",
"2. Создать новую запись (возможно, это другой человек)",
"3. Пропустить добавление",
],
};
case "email":
return {
action: "manual",
message: `⚠️ Конфликт email: ${conflictInfo.message}`,
suggestedActions: [
"1. Обновить существующую запись с новым email",
"2. Создать новую запись с другим email",
"3. Пропустить добавление",
],
};
case "similar":
return {
action: "manual",
message: `⚠️ Возможный дубликат: ${conflictInfo.message}`,
suggestedActions: [
"1. Проверить, не является ли это дубликатом",
"2. Создать новую запись (разные люди)",
"3. Пропустить добавление",
],
};
default:
return {
action: "create",
message: "Создаём нового сотрудника",
};
}
}
/**
* Принудительное создание сотрудника (игнорируя конфликты)
*/
async createEmployeeForce(createDto: CreateEmployeeDTO): Promise<unknown> {
const context = ErrorHandler.createErrorContext(
"createEmployeeForce",
`${this.baseUrl}/employees`,
"POST",
undefined,
createDto.companyId
);
return ErrorHandler.executeWithResilience(
async () => {
const url = `${this.baseUrl}/employees`;
const headers = {
Authorization: this.apiKey,
Accept: "application/json",
"Content-Type": "application/json",
};
console.log("[DEBUG] Force Create Employee Request", {
method: "POST",
url,
headers: { ...headers, Authorization: "***" },
body: createDto,
});
const response = await axios.post(url, createDto, { headers });
return response.data;
},
context,
{
maxRetries: 2, // Меньше попыток для принудительного создания
baseDelayMs: 1000,
}
);
}
/**
* Обновление существующего сотрудника
*/
async updateEmployee(employeeId: string, updateDto: UpdateEmployeeDTO): Promise<unknown> {
const context = ErrorHandler.createErrorContext("updateEmployee", `${this.baseUrl}/employees/${employeeId}`, "PUT");
return ErrorHandler.executeWithResilience(async () => {
const url = `${this.baseUrl}/employees/${employeeId}`;
const headers = {
Authorization: this.apiKey,
Accept: "application/json",
"Content-Type": "application/json",
};
console.log("[DEBUG] Update Employee Request", {
method: "PUT",
url,
headers: { ...headers, Authorization: "***" },
body: updateDto,
});
const response = await axios.put(url, updateDto, { headers });
return response.data;
}, context);
}
}