Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
4 changes: 4 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SettingService>;
let service: SupportEscalationService;

beforeEach(() => {
settingService = createMock<SettingService>();
service = new SupportEscalationService(
createMock<HttpService>(),
settingService,
createMock<SupportIssueRepository>(),
createMock<SupportMessageRepository>(),
);
settingService.get.mockResolvedValue('555'); // a DB binding exists
});

it('prefers the deployment-pinned chatId over the DB binding', async () => {
(ConfigModule as Record<string, unknown>).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<string, unknown>).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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ export class SupportEscalationService {
}

async getBoundChatId(): Promise<string | undefined> {
// 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);
}

Expand Down
Loading