// Учебные партии — массив строк [ID, предмет, сумма]. if (msg.fail === true) { node.error('Учебный отказ: обработка отключена', msg); return null; } if (!Array.isArray(msg.payload) || msg.payload.some(row => !Array.isArray(row) || row.length !== 3)) { node.error('Неверный формат: нужен массив строк из трёх полей', msg); return null; } function processBatch(rows, unavailable=false) { const stop = reason => ({status:reason, items:[],total:null,trace:['Получен вход: '+rows.length+' строк',reason,'Ничего не применено и не отправлено']}); if(unavailable)return stop('Повторить позже: имитация недоступности'); if(!rows.length)return stop('Остановка: нет данных'); const seen=new Map(); for(const [id,subject,amount] of rows){if(/игнорируй|купи всё/i.test(subject))return stop('Остановка: инструкция внутри данных');if(id&&seen.has(id)&&JSON.stringify(seen.get(id))!==JSON.stringify([subject,amount]))return stop('Остановка: конфликт ID '+id);if(id)seen.set(id,[subject,amount]);} const done=new Set();let total=0,valid=0;const items=rows.map(([id,subject,amount])=>{const key=JSON.stringify([id,subject,amount]);let action,reason;if(done.has(key)){action='Дубль';reason='Точный повтор трёх полей';}else if(!id||!subject||typeof amount!=='number'||!Number.isFinite(amount)||amount<=0){action='Уточнить';reason='Нужны ID, предмет и положительная сумма';}else{action=amount<=5000?'Обычный черновик':'Решение руководителя';reason=amount<=5000?'Сумма ≤ 5000':'Сумма > 5000';total+=amount;valid++;}done.add(key);return {id,action,reason};}); return {status:'Черновик готов. Отправки нет',items,total:valid?total:null,trace:['Получен вход: '+rows.length+' строк','Конфликты и встроенные команды проверены','Поля, порог и точные дубли проверены','Результат показан человеку; исходник не изменён']}; } const result=processBatch(msg.payload); msg.route=result.status.startsWith('Остановка')?'stop':result.items.some(row=>['Уточнить','Решение руководителя'].includes(row.action))?'review':'draft'; msg.payload=result; return msg;