From d5c40dd0f9eb81109f7eba05d3f6f0e547af07ac Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:07:01 +0200 Subject: [PATCH] feat(support): allow pinning the escalation chat id via SUPPORT_ESCALATION_CHAT_ID (#4142) The Telegram escalation target chat id was only settable through the runtime POST escalation/telegram-bind call, which writes a DB setting. Add an optional SUPPORT_ESCALATION_CHAT_ID env var: a non-secret chat id (useless without the bot token) that the deployment can pin, so the target becomes versioned, reviewed configuration instead of manual runtime state. When set it takes precedence in getBoundChatId(); when unset the DB binding still applies. --- .env.example | 1 + src/config/config.ts | 4 +++ .../support-escalation.service.spec.ts | 30 +++++++++++++++++++ .../services/support-escalation.service.ts | 5 ++++ 4 files changed, 40 insertions(+) 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); }