feat: enhance Telegram import functionality to support delays and cancellations
- Updated telegramCancellationParser to include parsing for delay notices. - Added new test cases for handling delay messages and stop-point cancellations. - Modified telegramImport integration tests to validate new delay handling. - Enhanced documentation to reflect the addition of delay notifications. - Updated TelegramImportPanel component to accommodate new alert types. - Introduced new fixture files for testing delay and cancellation messages.
This commit is contained in:
@@ -378,3 +378,128 @@ test('legacy per-direction Telegram alerts consolidate on edit without repeating
|
||||
assert.match(alerts[0].message, /Зодчего Иоанна/);
|
||||
} finally { await db.close(); }
|
||||
});
|
||||
|
||||
async function setupRoute4() {
|
||||
const context = await setup();
|
||||
await context.pool.query("UPDATE routes SET route_number='4' WHERE city_id=1");
|
||||
await context.pool.query("UPDATE stops SET name=CASE id WHEN 1 THEN 'Мариненко' ELSE 'Боровуха' END WHERE city_id=1");
|
||||
context.clock('2026-10-08T16:20:00Z');
|
||||
return context;
|
||||
}
|
||||
|
||||
async function route4Message(id, fixture = 'telegram-delay-route-4.txt') {
|
||||
const text = await fs.readFile(path.join(__dirname, 'fixtures', fixture), 'utf8');
|
||||
return { ...message(id, text), publishedAt: fixture.includes('delay') ? '2026-10-08T16:14:01Z' : '2026-10-08T02:03:13Z' };
|
||||
}
|
||||
|
||||
async function seedLegacyReview(pool, input, parsed, reasons) {
|
||||
return (await pool.query(`INSERT INTO telegram_import_messages
|
||||
(city_id,channel_id,channel_username,message_id,published_at,content_hash,raw_text,status,reasons,parsed)
|
||||
VALUES (1,$1,$2,$3,$4,$5,$6,'review',$7::jsonb,$8::jsonb) RETURNING id`,
|
||||
[input.channelId,input.channelUsername,input.messageId,input.publishedAt,contentHash(input),input.text,JSON.stringify(reasons),JSON.stringify(parsed)])).rows[0].id;
|
||||
}
|
||||
|
||||
test('screenshot reviews reprocess unchanged text into correctly typed alerts with no duplicates', { skip: !modulePath }, async () => {
|
||||
const { db, pool, service } = await setupRoute4();
|
||||
try {
|
||||
const delayed = await route4Message(1800);
|
||||
const cancelled = await route4Message(1801, 'telegram-stop-point-cancellation.txt');
|
||||
await seedLegacyReview(pool, delayed, { status: 'ignored', groups: [], plans: [] }, ['Текст исходной публикации изменился: отмена больше не распознаётся.']);
|
||||
await seedLegacyReview(pool, cancelled, { status: 'review', groups: [], plans: [] }, ['Некорректная дата отмены.']);
|
||||
for (const input of [delayed, cancelled]) {
|
||||
assert.equal((await service.processMessage(input, 1)).status, 'published');
|
||||
assert.equal((await service.processMessage(input, 1)).status, 'unchanged');
|
||||
}
|
||||
const rows = await service.list(1);
|
||||
const delay = rows.find(row => row.message_id === delayed.messageId);
|
||||
const cancellation = rows.find(row => row.message_id === cancelled.messageId);
|
||||
assert.deepEqual(delay.reasons, []);
|
||||
assert.deepEqual(cancellation.reasons, []);
|
||||
assert.equal(delay.parsed.alertType, 'delay');
|
||||
assert.equal(cancellation.parsed.alertType, 'cancellation');
|
||||
assert.equal(delay.alert_ids.length, 1);
|
||||
assert.equal(cancellation.alert_ids.length, 1);
|
||||
assert.match(delay.drafts[0].title, /Задержка/);
|
||||
assert.match(delay.drafts[0].message, /Мариненко — 19:05\nБоровуха — 20:00/);
|
||||
assert.match(cancellation.drafts[0].message, /Мариненко — 06:00, 07:30\nБоровуха — 06:40, 08:15/);
|
||||
const alerts = (await pool.query('SELECT * FROM alerts WHERE notification_pending=true ORDER BY id')).rows;
|
||||
assert.deepEqual(alerts.map(alert => alert.alert_type), ['delay', 'cancellation']);
|
||||
assert.ok(alerts.every(alert => alert.end_time.toISOString() === '2026-10-08T21:00:00.000Z'));
|
||||
assert.equal((await pool.query('SELECT count(*)::int AS n FROM telegram_message_alerts')).rows[0].n, 2);
|
||||
} finally { await db.close(); }
|
||||
});
|
||||
|
||||
test('editing a cancellation into a delay updates its type and content on the same delivered alert', { skip: !modulePath }, async () => {
|
||||
const { db, pool, service } = await setupRoute4();
|
||||
try {
|
||||
const input = await route4Message(1802, 'telegram-stop-point-cancellation.txt');
|
||||
await service.processMessage(input, 1);
|
||||
const row = (await service.list(1))[0];
|
||||
const id = row.alert_ids[0];
|
||||
await pool.query('UPDATE alerts SET notification_sent_at=now() WHERE id=$1', [id]);
|
||||
const delay = await route4Message(1802);
|
||||
await service.processMessage({ ...input, text: delay.text, editedAt: '2026-10-08T16:15:00Z' }, 1);
|
||||
const updated = (await pool.query('SELECT * FROM alerts WHERE id=$1', [id])).rows[0];
|
||||
assert.equal(updated.alert_type, 'delay');
|
||||
assert.match(updated.title, /Задержка/);
|
||||
assert.match(updated.message, /Мариненко — 19:05\nБоровуха — 20:00/);
|
||||
assert.ok(updated.notification_sent_at, 'the delivered push is not repeated');
|
||||
assert.deepEqual((await service.list(1))[0].alert_ids, [id]);
|
||||
assert.equal((await service.processMessage(input, 1)).status, 'unchanged', 'older source cannot restore the cancellation');
|
||||
} finally { await db.close(); }
|
||||
});
|
||||
|
||||
test('manual Telegram review defaults to delay, permits an explicit type choice, and rejects unrelated types', { skip: !modulePath }, async () => {
|
||||
const { db, pool, service } = await setupRoute4();
|
||||
service.config = { ...config, autoPublish: false };
|
||||
try {
|
||||
const input = await route4Message(1803);
|
||||
assert.equal((await service.processMessage(input, 1)).status, 'review');
|
||||
const row = (await service.list(1))[0];
|
||||
const values = { action: 'publish', content_hash: row.content_hash, service_date: '2026-10-08', route_numbers: ['4'], ...row.drafts[0] };
|
||||
await assert.rejects(service.review(row.id, 1, { ...values, alert_type: 'info' }, 1), error => error.status === 400);
|
||||
await service.review(row.id, 1, values, 1);
|
||||
const saved = (await pool.query('SELECT * FROM alerts WHERE notification_pending=true')).rows[0];
|
||||
assert.equal(saved.alert_type, 'delay');
|
||||
assert.equal(saved.end_time.toISOString(), '2026-10-08T21:00:00.000Z');
|
||||
await service.review(row.id, 1, { ...values, alert_type: 'cancellation', title: 'Уточнённая отмена', message: 'Уточнённый текст' }, 1);
|
||||
assert.equal((await pool.query('SELECT alert_type FROM alerts WHERE id=$1', [saved.id])).rows[0].alert_type, 'cancellation');
|
||||
assert.equal((await service.list(1))[0].parsed.alertType, 'cancellation');
|
||||
} finally { await db.close(); }
|
||||
});
|
||||
|
||||
test('past screenshot messages become expired on parser upgrade and cannot send a new push after midnight', { skip: !modulePath }, async () => {
|
||||
const { db, pool, service, clock } = await setupRoute4();
|
||||
clock('2026-10-08T21:00:00Z');
|
||||
try {
|
||||
for (const [id, fixture] of [[1804, 'telegram-delay-route-4.txt'], [1805, 'telegram-stop-point-cancellation.txt']]) {
|
||||
const input = await route4Message(id, fixture);
|
||||
await seedLegacyReview(pool, input, { status: 'review', groups: [] }, ['Старая ошибка']);
|
||||
assert.equal((await service.processMessage(input, 1)).status, 'expired');
|
||||
assert.equal((await service.processMessage(input, 1)).status, 'unchanged');
|
||||
}
|
||||
const rows = await service.list(1);
|
||||
assert.ok(rows.every(row => row.status === 'expired' && row.alert_ids.length === 0));
|
||||
assert.ok(rows.every(row => row.parsed.serviceDate === '2026-10-08'));
|
||||
assert.equal((await pool.query('SELECT count(*)::int AS n FROM alerts WHERE notification_pending=true')).rows[0].n, 0);
|
||||
const delay = rows.find(row => row.parsed.alertType === 'delay');
|
||||
await assert.rejects(service.review(delay.id, 1, { action: 'publish', content_hash: delay.content_hash, service_date: '2026-10-08', route_numbers: ['4'], title: 'Опоздание', message: 'Текст' }, 1), error => error.status === 410);
|
||||
} finally { await db.close(); }
|
||||
});
|
||||
|
||||
test('parser upgrade refreshes ambiguous delay review once and does not auto-publish attachments', { skip: !modulePath }, async () => {
|
||||
const { db, pool, service } = await setupRoute4();
|
||||
try {
|
||||
const input = { ...await route4Message(1806), hasMedia: true };
|
||||
await seedLegacyReview(pool, input, { status: 'ignored', groups: [] }, ['Отмена не распознана']);
|
||||
assert.equal((await service.processMessage(input, 1)).status, 'review');
|
||||
const row = (await service.list(1))[0];
|
||||
assert.equal(row.parsed.alertType, 'delay');
|
||||
assert.match(row.reasons[0], /вложение/);
|
||||
assert.equal(row.alert_ids.length, 0);
|
||||
assert.equal((await service.processMessage(input, 1)).status, 'unchanged');
|
||||
await service.review(row.id, 1, { action: 'dismiss', content_hash: row.content_hash }, 1);
|
||||
assert.equal((await service.processMessage({ ...input, hasMedia: false, editedAt: '2026-10-08T16:16:00Z' }, 1)).status, 'dismissed');
|
||||
assert.equal((await pool.query('SELECT count(*)::int AS n FROM alerts WHERE notification_pending=true')).rows[0].n, 0);
|
||||
} finally { await db.close(); }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user