const csv = require('csv-parser'); const fs = require('fs'); /** * Парсер отчёта по задолженности (CSV с разделителем ;). * Строки данных: № п/п (0), Лицевой счёт (1), Ответственный (4), Объект учета (5), Количество месяцев (8), последняя колонка — Общая задолженность. */ class DebtorReportProcessor { /** * Нормализация числа из отчёта: пробелы убрать, запятую заменить на точку. */ parseAmount(str) { if (str === undefined || str === null || str === '') return null; const s = String(str).replace(/\s/g, '').replace(',', '.').trim(); if (s === '' || s === '-') return null; const n = parseFloat(s); return Number.isFinite(n) ? n : null; } /** * Проверка: строка похожа на строку данных (колонка 0 — число, колонка 1 — лицевой счёт вида 00-000000001). */ isDataRow(cells) { if (!cells || cells.length < 6) return false; const col0 = String(cells[0] || '').trim(); const col1 = String(cells[1] || '').trim(); if (col1 === '' || col1.toLowerCase() === 'итого') return false; const num0 = parseInt(col0, 10); const isNum = col0 !== '' && !Number.isNaN(num0) && num0 >= 1; const looksLikeAccount = /^[\d\-]+$/.test(col1) || /^\d{2}-\d{9}$/.test(col1.replace(/\s/g, '')); return isNum && (looksLikeAccount || col1.length >= 5); } /** * Парсинг одной строки данных в объект для БД. */ parseDataRow(cells, rowIndex) { const account = String(cells[1] || '').trim(); const responsibleName = String(cells[4] || '').trim(); const objectAddress = String(cells[5] || '').trim(); const monthsRaw = String(cells[8] || '').trim(); const monthsDebt = monthsRaw === '' ? null : (parseInt(monthsRaw, 10) || null); const lastCol = cells[cells.length - 1]; const totalDebt = this.parseAmount(lastCol); return { row_index: rowIndex, account, responsible_name: responsibleName, object_address: objectAddress, months_debt: monthsDebt, total_debt: totalDebt != null ? totalDebt : 0, }; } /** * Парсинг CSV-файла отчёта по задолженности. * Возвращает массив объектов { row_index, account, responsible_name, object_address, months_debt, total_debt }. */ async parseDebtorReportCSV(filePath) { return new Promise((resolve, reject) => { const rows = []; let rowIndex = 0; fs.createReadStream(filePath, { encoding: 'utf8' }) .pipe(csv({ separator: ';', headers: false })) .on('data', (row) => { const cells = Object.values(row); if (rowIndex === 0 && cells[0] !== undefined) { const first = String(cells[0]).replace(/^\uFEFF/, ''); cells[0] = first; } if (this.isDataRow(cells)) { rows.push(this.parseDataRow(cells, rowIndex)); } rowIndex++; }) .on('end', () => resolve(rows)) .on('error', reject); }); } /** * Парсинг XLSX: чтение первого листа, поиск строк данных по тем же правилам. */ async parseDebtorReportXLSX(filePath) { const XLSX = require('xlsx'); const workbook = XLSX.readFile(filePath); const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; const rawData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: '' }); const rows = []; for (let i = 0; i < rawData.length; i++) { const cells = rawData[i] || []; if (this.isDataRow(cells)) { rows.push(this.parseDataRow(cells, i)); } } return rows; } /** * Единая точка входа: парсинг по типу файла. */ async parseDebtorReport(filePath, fileType) { if (fileType === 'CSV') { return this.parseDebtorReportCSV(filePath); } if (fileType === 'XLSX') { return this.parseDebtorReportXLSX(filePath); } throw new Error('Поддерживаются только CSV и XLSX'); } } module.exports = DebtorReportProcessor;