289 lines
7.8 KiB
TypeScript
289 lines
7.8 KiB
TypeScript
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<OrganizationData | null> {
|
||
try {
|
||
const response = await axios.post<DaDataResponse>(
|
||
`${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
|
||
}
|
||
}
|