diff --git a/.env.example b/.env.example index 76af0f4c66..f62fa5f470 100644 --- a/.env.example +++ b/.env.example @@ -124,6 +124,7 @@ SUPPORT_MESSAGE_NAME= SUPPORT_MESSAGE_MAIL= SUPPORT_MESSAGE_BANNER= SUPPORT_TELEGRAM_BOT_TOKEN= +SUPPORT_ESCALATION_CHAT_ID= ALBY_CLIENT_ID= ALBY_CLIENT_SECRET= diff --git a/src/config/config.ts b/src/config/config.ts index 621e430ce5..a093976aaa 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -578,6 +578,10 @@ export class Configuration { issueOnHoldExpiry: 14, //days escalation: { telegramBotToken: process.env.SUPPORT_TELEGRAM_BOT_TOKEN, + // Escalation target group. A Telegram chat id is non-secret config (useless without the bot + // token), so the deployment environment can pin it; when set it takes precedence over the + // runtime bind — a versioned/reviewed target instead of a manual getUpdates binding. + chatId: process.env.SUPPORT_ESCALATION_CHAT_ID, slaHours: 24, // customer waiting longer than this escalates }, }; diff --git a/src/subdomains/supporting/support-issue/services/__tests__/support-escalation.service.spec.ts b/src/subdomains/supporting/support-issue/services/__tests__/support-escalation.service.spec.ts index 9dff9e148c..1fecde8488 100644 --- a/src/subdomains/supporting/support-issue/services/__tests__/support-escalation.service.spec.ts +++ b/src/subdomains/supporting/support-issue/services/__tests__/support-escalation.service.spec.ts @@ -82,6 +82,36 @@ describe('SupportEscalationService.bindGroupChat', () => { }); }); +// Guards the chat-id precedence: a deployment-pinned SUPPORT_ESCALATION_CHAT_ID must win over the +// runtime DB binding, so the escalation target is versioned config rather than manual runtime state. +describe('SupportEscalationService.getBoundChatId', () => { + let settingService: DeepMocked; + let service: SupportEscalationService; + + beforeEach(() => { + settingService = createMock(); + service = new SupportEscalationService( + createMock(), + settingService, + createMock(), + createMock(), + ); + settingService.get.mockResolvedValue('555'); // a DB binding exists + }); + + it('prefers the deployment-pinned chatId over the DB binding', async () => { + (ConfigModule as Record).Config = { support: { escalation: { chatId: '-100999' } } }; + expect(await service.getBoundChatId()).toBe('-100999'); + expect(settingService.get).not.toHaveBeenCalled(); + }); + + it('falls back to the DB binding when no chatId is pinned', async () => { + (ConfigModule as Record).Config = { support: { escalation: {} } }; + expect(await service.getBoundChatId()).toBe('555'); + expect(settingService.get).toHaveBeenCalledWith('supportEscalationChatId'); + }); +}); + // Guards the escalation detection: a ticket escalates once the customer wrote last and has waited past the // SLA, and is then de-duplicated per waiting cycle so the group is not spammed every cron run. describe('SupportEscalationService.checkEscalations', () => { diff --git a/src/subdomains/supporting/support-issue/services/support-escalation.service.ts b/src/subdomains/supporting/support-issue/services/support-escalation.service.ts index 2cc3220e46..cfc8855cb5 100644 --- a/src/subdomains/supporting/support-issue/services/support-escalation.service.ts +++ b/src/subdomains/supporting/support-issue/services/support-escalation.service.ts @@ -183,6 +183,11 @@ export class SupportEscalationService { } async getBoundChatId(): Promise { + // A deployment-pinned chat id (SUPPORT_ESCALATION_CHAT_ID) is authoritative and wins over the + // runtime DB binding, so the escalation target is versioned/reviewed config rather than a manual + // getUpdates bind. When it is unset, the DB binding (POST escalation/telegram-bind) still applies. + const pinned = Config.support.escalation.chatId; + if (pinned) return pinned; return this.settingService.get(CHAT_ID_KEY); }