import axios from 'axios' export interface DaDataCompany { value: string unrestricted_value: string data: { kpp?: string management?: { name?: string post?: string } hid: string type: string state?: { status?: string actuality_date?: number registration_date?: number liquidation_date?: number } opf?: { code?: string full?: string short?: string } name: { full_with_opf?: string short_with_opf?: string full?: string short?: string } inn: string ogrn?: string ogrn_date?: number okpo?: string okato?: string oktmo?: string okved?: string employee_count?: number phones?: object[] emails?: object[] finance?: { revenue?: number tax_system?: string } address?: { value?: string unrestricted_value?: string data?: { region_with_type?: string city_with_type?: string } } } } interface DaDataResponse { suggestions: DaDataCompany[] } export interface OrganizationData { inn: string kpp?: string name: string fullName: string address: string addressFull?: string ogrn?: string ogrnDate?: Date isActive: boolean type: 'FULFILLMENT' | 'SELLER' // Статус организации status?: string actualityDate?: Date registrationDate?: Date liquidationDate?: Date // Руководитель managementName?: string managementPost?: string // ОПФ opfCode?: string opfFull?: string opfShort?: string // Коды статистики okato?: string oktmo?: string okpo?: string okved?: string // Контакты phones?: object[] emails?: object[] // Финансовые данные employeeCount?: number revenue?: bigint taxSystem?: string rawData: DaDataCompany } export class DaDataService { private apiKey: string private apiUrl: string constructor() { this.apiKey = process.env.DADATA_API_KEY! this.apiUrl = process.env.DADATA_API_URL! if (!this.apiKey || !this.apiUrl) { throw new Error('DaData API credentials not configured') } } /** * Получает информацию об организации по ИНН */ async getOrganizationByInn(inn: string): Promise { try { const response = await axios.post( `${this.apiUrl}/findById/party`, { query: inn, count: 1 }, { headers: { 'Authorization': `Token ${this.apiKey}`, 'Content-Type': 'application/json', 'Accept': 'application/json' } } ) if (!response.data?.suggestions?.length) { return null } const company = response.data.suggestions[0] // Определяем тип организации на основе ОПФ const organizationType = this.determineOrganizationType(company) return { inn: company.data.inn, kpp: company.data.kpp || undefined, name: company.data.name.short || company.data.name.full || 'Название не указано', fullName: company.data.name.full_with_opf || '', address: company.data.address?.value || '', addressFull: company.data.address?.unrestricted_value || undefined, ogrn: company.data.ogrn || undefined, ogrnDate: this.parseDate(company.data.ogrn_date), // Статус организации status: company.data.state?.status, actualityDate: this.parseDate(company.data.state?.actuality_date), registrationDate: this.parseDate(company.data.state?.registration_date), liquidationDate: this.parseDate(company.data.state?.liquidation_date), // Руководитель managementName: company.data.management?.name, managementPost: company.data.management?.post, // ОПФ opfCode: company.data.opf?.code, opfFull: company.data.opf?.full, opfShort: company.data.opf?.short, // Коды статистики okato: company.data.okato, oktmo: company.data.oktmo, okpo: company.data.okpo, okved: company.data.okved, // Контакты phones: company.data.phones || undefined, emails: company.data.emails || undefined, // Финансовые данные employeeCount: company.data.employee_count || undefined, revenue: company.data.finance?.revenue ? BigInt(company.data.finance.revenue) : undefined, taxSystem: company.data.finance?.tax_system || undefined, isActive: company.data.state?.status === 'ACTIVE', type: organizationType, rawData: company } } catch (error) { console.error('Error fetching organization data from DaData:', error) return null } } /** * Безопасно парсит дату из timestamp, возвращает undefined для некорректных дат */ private parseDate(timestamp?: number): Date | undefined { if (!timestamp) return undefined try { const date = new Date(timestamp * 1000) // Проверяем, что дата валидна и разумна (между 1900 и 2100 годами) if (isNaN(date.getTime()) || date.getFullYear() < 1900 || date.getFullYear() > 2100) { return undefined } return date } catch { return undefined } } /** * Определяет тип организации на основе ОПФ (организационно-правовая форма) */ private determineOrganizationType(company: DaDataCompany): 'FULFILLMENT' | 'SELLER' { const opfCode = company.data.opf?.code // Индивидуальные предприниматели чаще работают как селлеры if (company.data.type === 'INDIVIDUAL' || opfCode === '50102') { return 'SELLER' } // ООО, АО и другие юридические лица чаще работают с фулфилментом return 'FULFILLMENT' } /** * Валидирует ИНН по контрольной сумме */ validateInn(inn: string): boolean { const digits = inn.replace(/\D/g, '') if (digits.length !== 10 && digits.length !== 12) { return false } // Проверяем контрольную сумму для 10-значного ИНН (юридические лица) if (digits.length === 10) { const checksum = this.calculateInn10Checksum(digits) return checksum === parseInt(digits[9]) } // Проверяем контрольную сумму для 12-значного ИНН (ИП) if (digits.length === 12) { const checksum1 = this.calculateInn12Checksum1(digits) const checksum2 = this.calculateInn12Checksum2(digits) return checksum1 === parseInt(digits[10]) && checksum2 === parseInt(digits[11]) } return false } private calculateInn10Checksum(inn: string): number { const weights = [2, 4, 10, 3, 5, 9, 4, 6, 8] let sum = 0 for (let i = 0; i < 9; i++) { sum += parseInt(inn[i]) * weights[i] } return sum % 11 % 10 } private calculateInn12Checksum1(inn: string): number { const weights = [7, 2, 4, 10, 3, 5, 9, 4, 6, 8] let sum = 0 for (let i = 0; i < 10; i++) { sum += parseInt(inn[i]) * weights[i] } return sum % 11 % 10 } private calculateInn12Checksum2(inn: string): number { const weights = [3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8] let sum = 0 for (let i = 0; i < 11; i++) { sum += parseInt(inn[i]) * weights[i] } return sum % 11 % 10 } }