Skip to content

Commit 1171166

Browse files
committed
feat: Add universal notification system from Kogotochki
- NotificationService with template-based message generation - NotificationConnector with retry logic and batch processing - User preference management for notification categories - Full test coverage for all components - Integration examples and migration guide - Support for multiple notification types (auction, balance, service, system) This system was battle-tested in the Kogotochki bot and provides a robust foundation for handling notifications across different platforms.
1 parent 745cf54 commit 1171166

7 files changed

+1003
-0
lines changed

src/contrib/README.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Universal Notification System
2+
3+
A comprehensive notification system with retry logic, batch processing, and user preferences management.
4+
5+
## Features
6+
7+
- 🔄 **Retry Logic**: Automatic retry mechanism for failed notifications
8+
- 📦 **Batch Processing**: Efficient batch sending for mass notifications
9+
- ⚙️ **User Preferences**: Granular control over notification categories
10+
- 🛡️ **Error Handling**: Graceful handling of blocked users and errors
11+
- 📊 **Monitoring**: Built-in Sentry integration for error tracking
12+
- 🌐 **Platform Agnostic**: Easy to adapt for different messaging platforms
13+
14+
## Components
15+
16+
### 1. NotificationService
17+
Main service that manages notification templates and business logic.
18+
19+
**Key features:**
20+
- Template-based message generation
21+
- Support for multiple notification types
22+
- Integration with user preferences
23+
- Auction result notifications
24+
25+
### 2. NotificationConnector
26+
Low-level connector that handles the actual message delivery.
27+
28+
**Key features:**
29+
- Retry mechanism with exponential backoff
30+
- Batch sending with configurable batch size
31+
- Detection of blocked users
32+
- Error tracking and reporting
33+
34+
### 3. User Preferences
35+
Flexible system for managing user notification settings.
36+
37+
**Categories:**
38+
- `auction` - Auction-related notifications
39+
- `balance` - Balance change notifications
40+
- `service` - Service status notifications
41+
- `system` - System announcements
42+
43+
### 4. UI Components
44+
Telegram-specific UI for managing notification settings.
45+
46+
**Features:**
47+
- Toggle all notifications on/off
48+
- Select specific notification categories
49+
- Inline keyboard interface
50+
- Real-time preference updates
51+
52+
## Usage Example
53+
54+
```typescript
55+
// Initialize services
56+
const notificationConnector = new NotificationConnector(telegramConnector);
57+
const notificationService = new NotificationService(notificationConnector, userService);
58+
59+
// Send auction win notification
60+
await notificationService.sendAuctionWinNotification(userId, {
61+
userId,
62+
serviceId,
63+
categoryId,
64+
position: 1,
65+
bidAmount: 100,
66+
roundId,
67+
timestamp: new Date(),
68+
});
69+
70+
// Send batch notifications
71+
await notificationService.sendBatchNotifications(
72+
[userId1, userId2, userId3],
73+
'System maintenance scheduled for tonight',
74+
'system'
75+
);
76+
```
77+
78+
## Integration Points
79+
80+
1. **Database Schema**
81+
- `users` table with `notification_enabled` and `notification_categories` columns
82+
- Support for JSON storage of category preferences
83+
84+
2. **Event System**
85+
- Integrates with auction processing events
86+
- Can be extended for other event types
87+
88+
3. **Error Monitoring**
89+
- Automatic Sentry integration
90+
- Detailed error context for debugging
91+
92+
## Testing
93+
94+
Comprehensive test suite covering:
95+
- All notification types
96+
- Retry logic
97+
- Batch processing
98+
- User preference management
99+
- Error scenarios
100+
101+
## Migration Guide
102+
103+
To integrate this system into your project:
104+
105+
1. Copy the notification service and connector
106+
2. Add notification columns to your users table
107+
3. Implement the messaging connector interface
108+
4. Add UI components for user preferences
109+
5. Configure notification templates for your use case
110+
111+
## Future Enhancements
112+
113+
- [ ] Email notification support
114+
- [ ] Push notification support
115+
- [ ] Notification scheduling
116+
- [ ] Rich media support
117+
- [ ] Analytics and delivery reports

src/contrib/example-integration.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Example integration for Wireframe platform
2+
3+
import type { INotificationService } from './notification.service.interface';
4+
import type { INotificationConnector } from './notification-connector';
5+
import { NotificationService } from './notification.service';
6+
import { NotificationConnector } from './notification-connector';
7+
8+
// Example: Telegram integration
9+
export class TelegramNotificationSetup {
10+
static async setup(
11+
telegramConnector: ITelegramConnector,
12+
userService: IUserService,
13+
): Promise<INotificationService> {
14+
// Create notification connector
15+
const notificationConnector = new NotificationConnector(telegramConnector);
16+
17+
// Create notification service
18+
const notificationService = new NotificationService(notificationConnector, userService);
19+
20+
return notificationService;
21+
}
22+
}
23+
24+
// Example: User preferences UI
25+
export const NotificationSettingsKeyboard = {
26+
getKeyboard(user: { notificationEnabled: boolean; notificationCategories?: string[] }) {
27+
const categories = user.notificationCategories || [];
28+
29+
return {
30+
inline_keyboard: [
31+
[
32+
{
33+
text: `${user.notificationEnabled ? '✅' : '❌'} All notifications`,
34+
callback_data: 'notification:toggle',
35+
},
36+
],
37+
...(user.notificationEnabled
38+
? [
39+
[
40+
{
41+
text: `${categories.includes('auction') ? '✅' : '⬜️'} Auction notifications`,
42+
callback_data: 'notification:category:auction',
43+
},
44+
],
45+
[
46+
{
47+
text: `${categories.includes('balance') ? '✅' : '⬜️'} Balance changes`,
48+
callback_data: 'notification:category:balance',
49+
},
50+
],
51+
[
52+
{
53+
text: `${categories.includes('service') ? '✅' : '⬜️'} Service status`,
54+
callback_data: 'notification:category:service',
55+
},
56+
],
57+
[
58+
{
59+
text: `${categories.includes('system') ? '✅' : '⬜️'} System messages`,
60+
callback_data: 'notification:category:system',
61+
},
62+
],
63+
]
64+
: []),
65+
[{ text: '← Back', callback_data: 'settings:main' }],
66+
],
67+
};
68+
},
69+
};
70+
71+
// Example: Database migration
72+
export const notificationMigration = `
73+
ALTER TABLE users ADD COLUMN IF NOT EXISTS notification_enabled BOOLEAN DEFAULT true;
74+
ALTER TABLE users ADD COLUMN IF NOT EXISTS notification_categories JSON;
75+
76+
-- Create index for efficient querying
77+
CREATE INDEX IF NOT EXISTS idx_users_notifications
78+
ON users(notification_enabled)
79+
WHERE notification_enabled = true;
80+
`;
81+
82+
// Example: Integration with event system
83+
export class NotificationEventHandler {
84+
constructor(private notificationService: INotificationService) {}
85+
86+
async handleAuctionComplete(event: AuctionCompleteEvent) {
87+
// Notify winners
88+
for (const winner of event.winners) {
89+
await this.notificationService.sendAuctionWinNotification(winner.userId, {
90+
userId: winner.userId,
91+
serviceId: winner.serviceId,
92+
categoryId: event.categoryId,
93+
position: winner.position as 1 | 2 | 3,
94+
bidAmount: winner.bidAmount,
95+
roundId: event.roundId,
96+
timestamp: new Date(),
97+
});
98+
}
99+
100+
// Notify outbid users
101+
for (const loser of event.losers) {
102+
await this.notificationService.sendAuctionRefundNotification(
103+
loser.userId,
104+
event.categoryId,
105+
loser.bidAmount,
106+
);
107+
}
108+
}
109+
}
110+
111+
// Type definitions for the example
112+
interface ITelegramConnector {
113+
sendMessage(chatId: number, text: string): Promise<void>;
114+
}
115+
116+
interface IUserService {
117+
getUsersWithNotificationCategory(category: string): Promise<Array<{ telegramId: number }>>;
118+
updateNotificationSettings(
119+
telegramId: number,
120+
enabled: boolean,
121+
categories?: string[],
122+
): Promise<void>;
123+
}
124+
125+
interface AuctionCompleteEvent {
126+
roundId: number;
127+
categoryId: string;
128+
winners: Array<{
129+
userId: number;
130+
serviceId: number;
131+
position: number;
132+
bidAmount: number;
133+
}>;
134+
losers: Array<{
135+
userId: number;
136+
bidAmount: number;
137+
}>;
138+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
3+
import { NotificationConnector } from '@/connectors/notification/notification-connector';
4+
import type { ITelegramConnector } from '@/connectors/telegram/interfaces/telegram-connector.interface';
5+
6+
describe('NotificationConnector', () => {
7+
let notificationConnector: NotificationConnector;
8+
let mockTelegramConnector: ITelegramConnector;
9+
10+
beforeEach(() => {
11+
vi.clearAllMocks();
12+
13+
mockTelegramConnector = {
14+
sendMessage: vi.fn().mockResolvedValue(undefined),
15+
} as unknown as ITelegramConnector;
16+
17+
notificationConnector = new NotificationConnector(mockTelegramConnector);
18+
});
19+
20+
describe('sendNotification', () => {
21+
it('should send notification successfully', async () => {
22+
const result = await notificationConnector.sendNotification(123456, 'Test message');
23+
24+
expect(result).toBe(true);
25+
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledWith(123456, 'Test message');
26+
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledTimes(1);
27+
});
28+
29+
it('should retry on failure', async () => {
30+
vi.mocked(mockTelegramConnector.sendMessage)
31+
.mockRejectedValueOnce(new Error('Temporary error'))
32+
.mockRejectedValueOnce(new Error('Temporary error'))
33+
.mockResolvedValueOnce(undefined);
34+
35+
const result = await notificationConnector.sendNotification(123456, 'Test message');
36+
37+
expect(result).toBe(true);
38+
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledTimes(3);
39+
});
40+
41+
it('should return false after max retries', async () => {
42+
vi.mocked(mockTelegramConnector.sendMessage).mockRejectedValue(new Error('Persistent error'));
43+
44+
const result = await notificationConnector.sendNotification(123456, 'Test message');
45+
46+
expect(result).toBe(false);
47+
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledTimes(3);
48+
});
49+
50+
it('should not retry if user blocked the bot', async () => {
51+
vi.mocked(mockTelegramConnector.sendMessage).mockRejectedValueOnce(
52+
new Error('Bot was blocked by the user'),
53+
);
54+
55+
const result = await notificationConnector.sendNotification(123456, 'Test message');
56+
57+
expect(result).toBe(false);
58+
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledTimes(1);
59+
});
60+
61+
it('should detect various blocked user errors', async () => {
62+
const blockedErrors = [
63+
'Bot was blocked by the user',
64+
'User is deactivated',
65+
'Chat not found',
66+
'Forbidden: bot was blocked',
67+
];
68+
69+
for (const errorMessage of blockedErrors) {
70+
vi.clearAllMocks();
71+
vi.mocked(mockTelegramConnector.sendMessage).mockRejectedValueOnce(new Error(errorMessage));
72+
73+
const result = await notificationConnector.sendNotification(123456, 'Test message');
74+
75+
expect(result).toBe(false);
76+
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledTimes(1);
77+
}
78+
});
79+
});
80+
81+
describe('sendBatchNotifications', () => {
82+
it('should send notifications in batches', async () => {
83+
const notifications = Array.from({ length: 75 }, (_, i) => ({
84+
userId: 100 + i,
85+
message: `Message ${i}`,
86+
}));
87+
88+
await notificationConnector.sendBatchNotifications(notifications);
89+
90+
// Should be called 75 times (once for each notification)
91+
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledTimes(75);
92+
93+
// Check first and last calls
94+
expect(mockTelegramConnector.sendMessage).toHaveBeenNthCalledWith(1, 100, 'Message 0');
95+
expect(mockTelegramConnector.sendMessage).toHaveBeenNthCalledWith(75, 174, 'Message 74');
96+
});
97+
98+
it('should handle failures gracefully in batch', async () => {
99+
// Set up mixed success/failure responses
100+
vi.mocked(mockTelegramConnector.sendMessage)
101+
.mockResolvedValueOnce(undefined) // First message success
102+
.mockRejectedValue(new Error('Failed')); // All others fail
103+
104+
const notifications = [
105+
{ userId: 111, message: 'Message 1' },
106+
{ userId: 222, message: 'Message 2' },
107+
{ userId: 333, message: 'Message 3' },
108+
];
109+
110+
// Just verify it completes without throwing
111+
await expect(
112+
notificationConnector.sendBatchNotifications(notifications),
113+
).resolves.toBeUndefined();
114+
115+
// Verify at least the first successful call was made
116+
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledWith(111, 'Message 1');
117+
});
118+
119+
it('should delay between batches', async () => {
120+
const notifications = Array.from({ length: 31 }, (_, i) => ({
121+
userId: 100 + i,
122+
message: `Message ${i}`,
123+
}));
124+
125+
const start = Date.now();
126+
await notificationConnector.sendBatchNotifications(notifications);
127+
const duration = Date.now() - start;
128+
129+
// Should have delayed at least 1000ms between batches
130+
expect(duration).toBeGreaterThanOrEqual(1000);
131+
expect(mockTelegramConnector.sendMessage).toHaveBeenCalledTimes(31);
132+
});
133+
134+
it('should handle empty notifications array', async () => {
135+
await notificationConnector.sendBatchNotifications([]);
136+
137+
expect(mockTelegramConnector.sendMessage).not.toHaveBeenCalled();
138+
});
139+
});
140+
});

0 commit comments

Comments
 (0)