diff --git a/logic/hash.js b/logic/hash.js new file mode 100644 index 0000000..6ab40d2 --- /dev/null +++ b/logic/hash.js @@ -0,0 +1,28 @@ +function stableStringify(value) { + if (value === null || typeof value !== 'object') return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`; + const keys = Object.keys(value).sort(); + return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`; +} + +function djb2Hash(str) { + let hash = 5381; + for (let i = 0; i < str.length; i += 1) { + hash = ((hash << 5) + hash + str.charCodeAt(i)) >>> 0; + } + return hash.toString(16).padStart(8, '0'); +} + +const FINGERPRINT_FIELDS = ['status', 'tecnico_atribuido', 'categoria', 'entidade']; + +function buildContextFingerprint(ticket) { + const followups = (ticket.followups || []).map((f) => `${f.id}:${f.date}`).sort(); + const tasks = (ticket.tasks || []).map((t) => `${t.id}:${t.status}:${t.date}`).sort(); + const picked = {}; + for (const field of FINGERPRINT_FIELDS) picked[field] = ticket[field]; + picked.followups = followups; + picked.tasks = tasks; + return djb2Hash(stableStringify(picked)); +} + +module.exports = { stableStringify, djb2Hash, buildContextFingerprint }; diff --git a/logic/hash.test.js b/logic/hash.test.js new file mode 100644 index 0000000..b2a084b --- /dev/null +++ b/logic/hash.test.js @@ -0,0 +1,38 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const { buildContextFingerprint, stableStringify } = require('./hash'); + +function baseTicket(overrides) { + return Object.assign({ + status: 2, + tecnico_atribuido: 'Wagner', + categoria: 'Rede', + entidade: 'Matriz', + followups: [{ id: 1, date: '2026-08-20 10:00:00' }], + tasks: [{ id: 1, status: 1, date: '2026-08-19 09:00:00' }], + dias_aberto: 10, + dias_sem_interacao: 3, + }, overrides); +} + +test('alterar apenas dias_aberto/dias_sem_interacao NAO muda o hash', () => { + const a = buildContextFingerprint(baseTicket({ dias_aberto: 10, dias_sem_interacao: 3 })); + const b = buildContextFingerprint(baseTicket({ dias_aberto: 11, dias_sem_interacao: 4 })); + assert.equal(a, b); +}); + +test('novo followup MUDA o hash', () => { + const a = buildContextFingerprint(baseTicket({})); + const b = buildContextFingerprint(baseTicket({ followups: [{ id: 1, date: '2026-08-20 10:00:00' }, { id: 2, date: '2026-08-21 08:00:00' }] })); + assert.notEqual(a, b); +}); + +test('mudanca de tecnico MUDA o hash', () => { + const a = buildContextFingerprint(baseTicket({ tecnico_atribuido: 'Wagner' })); + const b = buildContextFingerprint(baseTicket({ tecnico_atribuido: 'Eric' })); + assert.notEqual(a, b); +}); + +test('stableStringify ignora ordem de chaves', () => { + assert.equal(stableStringify({ b: 1, a: 2 }), stableStringify({ a: 2, b: 1 })); +});