feat(notifications): add saved search email alert templates

Add the two missing Handlebars templates (saved_search_alert and
saved_search_digest) that are referenced by the real-time event handler
and daily digest cron but were never defined, causing a runtime crash.
Includes corresponding unit tests.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-11 01:59:03 +07:00
parent f8f2935f45
commit 80725ed81f
2 changed files with 83 additions and 2 deletions

View File

@@ -39,10 +39,52 @@ describe('TemplateService', () => {
expect(service.hasTemplate('unknown.template')).toBe(false);
});
it('getTemplateKeys returns all 9 template keys', () => {
it('render returns rendered subject and body for saved_search_alert template', () => {
const result = service.render('saved_search_alert', {
userName: 'Nguyễn Văn A',
searchName: 'Chung cư Quận 7',
listingTitle: 'Căn hộ 2PN tầng cao view sông',
listingPrice: '3.500.000.000',
listingDistrict: 'Quận 7',
listingCity: 'Hồ Chí Minh',
listingUrl: '/listings/abc123',
});
expect(result.subject).toBe('Tin mới phù hợp tìm kiếm "Chung cư Quận 7"');
expect(result.body).toContain('Nguyễn Văn A');
expect(result.body).toContain('Chung cư Quận 7');
expect(result.body).toContain('Căn hộ 2PN tầng cao view sông');
expect(result.body).toContain('3.500.000.000 VNĐ');
expect(result.body).toContain('Quận 7');
expect(result.body).toContain('/listings/abc123');
});
it('render returns rendered subject and body for saved_search_digest template', () => {
const result = service.render('saved_search_digest', {
userName: 'Trần Thị B',
searchName: 'Villa Thủ Đức',
matchCount: 3,
listings: [
{ title: 'Villa 1', price: '5.000.000.000', district: 'Thủ Đức', city: 'Hồ Chí Minh', url: '/listings/1' },
{ title: 'Villa 2', price: '7.000.000.000', district: 'Thủ Đức', city: 'Hồ Chí Minh', url: '/listings/2' },
],
});
expect(result.subject).toBe('3 bất động sản mới khớp tìm kiếm "Villa Thủ Đức"');
expect(result.body).toContain('Trần Thị B');
expect(result.body).toContain('Villa Thủ Đức');
expect(result.body).toContain('3');
expect(result.body).toContain('Villa 1');
expect(result.body).toContain('5.000.000.000 VNĐ');
expect(result.body).toContain('Villa 2');
expect(result.body).toContain('/listings/1');
expect(result.body).toContain('/listings/2');
});
it('getTemplateKeys returns all 11 template keys', () => {
const keys = service.getTemplateKeys();
expect(keys).toHaveLength(9);
expect(keys).toHaveLength(11);
expect(keys).toContain('user.registered');
expect(keys).toContain('agent.verified');
expect(keys).toContain('listing.approved');
@@ -52,5 +94,7 @@ describe('TemplateService', () => {
expect(keys).toContain('password.reset');
expect(keys).toContain('payment.confirmed');
expect(keys).toContain('subscription.expiring');
expect(keys).toContain('saved_search_alert');
expect(keys).toContain('saved_search_digest');
});
});

View File

@@ -75,6 +75,43 @@ const TEMPLATES: Record<string, TemplateDefinition> = {
body: `<h1>Gói đăng ký đã bị huỷ</h1>
<p>Gói <strong>{{planTier}}</strong> của bạn đã bị huỷ.</p>
<p>Bạn có thể đăng ký lại bất cứ lúc nào để tiếp tục sử dụng đầy đủ tính năng.</p>
<p>Trân trọng,<br/>Đội ngũ GoodGo</p>`,
},
'saved_search_alert': {
subject: 'Tin mới phù hợp tìm kiếm "{{searchName}}"',
body: `<h1>Xin chào {{userName}}!</h1>
<p>Có tin đăng mới phù hợp với tìm kiếm đã lưu <strong>"{{searchName}}"</strong> của bạn:</p>
<table style="width:100%;border-collapse:collapse;margin:16px 0;">
<tr style="background:#f8f9fa;">
<td style="padding:12px;border:1px solid #dee2e6;"><strong>{{listingTitle}}</strong></td>
</tr>
<tr>
<td style="padding:12px;border:1px solid #dee2e6;">
Giá: <strong>{{listingPrice}} VNĐ</strong><br/>
Khu vực: {{listingDistrict}}, {{listingCity}}
</td>
</tr>
</table>
<p><a href="{{listingUrl}}" style="display:inline-block;padding:10px 20px;background:#2563eb;color:#fff;text-decoration:none;border-radius:6px;">Xem chi tiết</a></p>
<p style="color:#6b7280;font-size:14px;">Bạn có thể tắt thông báo cho tìm kiếm này trong phần <a href="/saved-searches">Tìm kiếm đã lưu</a>.</p>
<p>Trân trọng,<br/>Đội ngũ GoodGo</p>`,
},
'saved_search_digest': {
subject: '{{matchCount}} bất động sản mới khớp tìm kiếm "{{searchName}}"',
body: `<h1>Xin chào {{userName}}!</h1>
<p>Có <strong>{{matchCount}}</strong> bất động sản mới phù hợp với tìm kiếm đã lưu <strong>"{{searchName}}"</strong> của bạn:</p>
<table style="width:100%;border-collapse:collapse;margin:16px 0;">
{{#each listings}}
<tr style="{{#if @first}}background:#f8f9fa;{{/if}}">
<td style="padding:12px;border:1px solid #dee2e6;">
<strong><a href="{{url}}" style="color:#2563eb;text-decoration:none;">{{title}}</a></strong><br/>
Giá: <strong>{{price}} VNĐ</strong> · {{district}}, {{city}}
</td>
</tr>
{{/each}}
</table>
<p><a href="/saved-searches" style="display:inline-block;padding:10px 20px;background:#2563eb;color:#fff;text-decoration:none;border-radius:6px;">Xem tất cả kết quả</a></p>
<p style="color:#6b7280;font-size:14px;">Bạn có thể tắt thông báo cho tìm kiếm này trong phần <a href="/saved-searches">Tìm kiếm đã lưu</a>.</p>
<p>Trân trọng,<br/>Đội ngũ GoodGo</p>`,
},
};