Files
ridebus-admin/backend/test/telegramCancellationParser.test.js
Zakhar Timoshenko 666a0b268f feat: implement stop alias management for Telegram import
- Added TelegramStopAliasService for handling stop aliases in the database.
- Created API endpoints for managing stop aliases (list, create, update, delete).
- Introduced TelegramStopAliases component in the frontend for user interaction with stop aliases.
- Updated TelegramImportService to utilize stop aliases for message processing.
- Enhanced telegramCancellationParser to resolve stop names using aliases.
- Added migration script to create telegram_stop_aliases table and seed initial data.
- Updated documentation to include details about stop alias functionality.
2026-10-03 01:31:11 +03:00

118 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const test = require('node:test');
const assert = require('node:assert/strict');
const { parseCancellation, resolveCancellationStops, matchCancellationRoutes, buildCancellationAlert, normalizeStopKey, dayBounds } = require('../src/services/telegramCancellationParser');
const text = `Уважаемые пассажиры!
Не будут выполняться рейсы: 
ПО МАРШРУТУ № 27 С ОСТАНОВКИ АВ/ВОКЗАЛ НА 15.43,16.43.
                С ОСТАНОВКИ ЗОДЧЕГО НА 15.58,16.58.
С УВАЖЕНИЕМ
Филиал "Автобусный парк №2 г.Полоцка" ОАО "Витебскоблавтотранс"`;
const parse = (value = text, publishedAt = '2026-10-02T12:52:00Z') => parseCancellation({ text: value, publishedAt });
test('carrier post preserves four individual departures and canonicalizes АВ/ВОКЗАЛ', () => {
const result = parse();
assert.equal(result.status, 'ready');
assert.equal(result.serviceDate, '2026-10-02');
assert.deepEqual(result.groups, [{ routeNumber: '27', departures: [
{ stop: 'Автовокзал', times: ['15:43', '16:43'] }, { stop: 'ЗОДЧЕГО', times: ['15:58', '16:58'] }
] }]);
});
test('implicit date comes from publication in Minsk, never from import day or edit date', () => {
assert.equal(parse(text, '2026-10-02T22:30:00Z').serviceDate, '2026-10-03');
});
test('tomorrow, numeric and Russian dates take precedence over publication day', () => {
for (const prefix of ['Завтра\n', '03.10.2026\n', 'На 03.10\n', '3 октября 2026 года\n']) {
assert.equal(parse(prefix + text).serviceDate, '2026-10-03', prefix);
}
assert.equal(parse('Завтра\n' + text, '2026-12-31T17:00:00Z').serviceDate, '2027-01-01');
assert.equal(parse('На 01.01\n' + text, '2026-12-31T17:00:00Z').serviceDate, '2027-01-01');
});
test('09.10 in a departure list remains a time', () => {
assert.deepEqual(parse(text.replace('15.43', '09.10')).groups[0].departures[0].times, ['09:10', '16:43']);
});
test('ambiguous dates, conditions, invalid times and unspecified departures require review', () => {
for (const value of [
'Сегодня и завтра\n' + text, 'На 31.02.2026\n' + text,
'С 02.10 по 05.10\n' + text, 'По пятницам\n' + text,
text.replace('15.43', '25.43'), text.replace('15.43', '15.99'),
'Не будут выполняться рейсы по маршруту №27.', text + '\nКроме отправления на 18:00.',
'Послезавтра\n' + text
]) assert.equal(parse(value).status, 'review', value);
});
test('news is ignored; a picture without text stays on review', () => {
assert.equal(parse('С праздником, уважаемые пассажиры!').status, 'ignored');
assert.equal(parseCancellation({ text: '', hasMedia: true, publishedAt: '2026-10-02T10:00:00Z' }).status, 'review');
});
test('review retains a recognized future date; attached media cannot be silently auto-published', () => {
const result = parse('Завтра\nНе будут выполняться рейсы по маршруту №27.');
assert.equal(result.status, 'review');
assert.equal(result.serviceDate, '2026-10-03');
assert.equal(parseCancellation({ text, hasMedia: true, publishedAt: '2026-10-02T12:00:00Z' }).status, 'review');
});
test('different route numbers in one post are separate groups', () => {
const result = parse(text + '\nПО МАРШРУТУ № 5А С ОСТАНОВКИ ЦЕНТР НА 18:00.');
assert.equal(result.status, 'ready');
assert.deepEqual(result.groups.map(g => g.routeNumber), ['27', '5а']);
});
test('route number with two directions is matched by origin stop, with no guess on missing origin', () => {
const routes = [
{ id: 1, route_number: '27', origin_stop: 'Автовокзал' },
{ id: 2, route_number: '27', origin_stop: 'Зодчего' },
{ id: 3, route_number: '5', origin_stop: 'Центр' }
];
const matched = matchCancellationRoutes(parse(), routes);
assert.deepEqual(matched.plans.map(p => p.routeId), [1, 2]);
assert.equal(matched.reasons.length, 0);
assert.equal(matchCancellationRoutes(parse(), routes.map(r => ({ ...r, origin_stop: '' }))).plans.length, 0);
assert.equal(matchCancellationRoutes(parse(), []).reasons.length, 1);
});
test('midnight expiry is an explicit UTC instant, including year boundary', () => {
assert.deepEqual(dayBounds('2026-10-02'), { start: '2026-10-01T21:00:00.000Z', end: '2026-10-02T21:00:00.000Z' });
assert.equal(dayBounds('2026-12-31').end, '2026-12-31T21:00:00.000Z');
const plan = matchCancellationRoutes(parse(), [{ id: 1, route_number: '27' }]).plans[0];
const alert = buildCancellationAlert(plan, '2026-10-02', 'https://t.me/avp2Polotsk/1166', '2026-10-02T12:52:00Z', 1);
assert.equal(alert.end_time, '2026-10-02T21:00:00.000Z');
assert.match(alert.message, /Автовокзал.*15:43, 16:43/);
assert.match(alert.title, /отдельных рейсов/);
});
module.exports = { carrierText: text };
test('dictionary selects the actual stop ID and uses its canonical name in concise alerts', () => {
const stops = [{ id: 11, name: 'Автовокзал' }, { id: 22, name: 'Зодчего Иоанна' }, { id: 23, name: 'Зодчего Иоанна' }];
const aliases = [{ normalized_alias: normalizeStopKey('ЗОДЧЕГО'), stop_id: 22 }];
const parsed = resolveCancellationStops(parse(), stops, aliases);
const routes = [
{ id: 1, route_number: '27', origin_stop_id: 11, origin_stop: 'Автовокзал' },
{ id: 2, route_number: '27', origin_stop_id: 22, origin_stop: 'Зодчего Иоанна' },
{ id: 3, route_number: '27', origin_stop_id: 23, origin_stop: 'Зодчего Иоанна' }
];
const matched = matchCancellationRoutes(parsed, routes);
assert.deepEqual(matched.plans.map(plan => plan.routeId), [1, 2]);
const alert = buildCancellationAlert(matched.plans[1], '2026-10-02', 'https://t.me/avp2Polotsk/1166', '2026-10-02T12:52Z', 1);
assert.equal(alert.message, '02.10.2026: не выполняются рейсы:\nЗодчего Иоанна — 15:58, 16:58\nИсточник: https://t.me/avp2Polotsk/1166');
assert.doesNotMatch(alert.message, /Уважаемые|С УВАЖЕНИЕМ/);
assert.equal(normalizeStopKey(' « ЗОД-ЧЕГО » '), normalizeStopKey('Зодчего'));
});
test('canonical names are taken from the catalog; inactive aliases and ambiguous platforms require review', () => {
const parsed = parse(text.replace('ЗОДЧЕГО', 'ЗОДЧЕГО ИОАННА'));
const stops = [{ id: 11, name: 'Автовокзал' }, { id: 22, name: 'Зодчего Иоанна' }];
assert.equal(resolveCancellationStops(parsed, stops, []).groups[0].departures[1].stop, 'Зодчего Иоанна');
const inactive = resolveCancellationStops(parse(), [stops[0]], [{ normalized_alias: 'зодчего', stop_id: 22 }]);
assert.match(matchCancellationRoutes(inactive, [{ id: 1, route_number: '27' }]).reasons[0], /отключена/);
const ambiguous = resolveCancellationStops(parsed, [...stops, { id: 23, name: 'Зодчего Иоанна' }], []);
const result = matchCancellationRoutes(ambiguous, [{ id: 1, route_number: '27', origin_stop: 'Зодчего Иоанна' }, { id: 2, route_number: '27', origin_stop: 'Зодчего Иоанна' }]);
assert.equal(result.plans.length, 0);
});